mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
Update admin navigation (1-1104-improved-menu-oss) (#4458)
Cleaner tabs navigation for admin tabs  Closes https://linear.app/unleash/issue/1-1104/improved-menu-oss
This commit is contained in:
parent
95f4f641b5
commit
8ee031e978
@ -5,64 +5,34 @@ import { AuthSettings } from './auth/AuthSettings';
|
|||||||
import { Billing } from './billing/Billing';
|
import { Billing } from './billing/Billing';
|
||||||
import FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
import FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
||||||
import { CorsAdmin } from './cors';
|
import { CorsAdmin } from './cors';
|
||||||
import { CreateGroup } from './groups/CreateGroup/CreateGroup';
|
|
||||||
import { EditGroupContainer } from './groups/EditGroup/EditGroup';
|
|
||||||
import { Group } from './groups/Group/Group';
|
|
||||||
import { GroupsAdmin } from './groups/GroupsAdmin';
|
import { GroupsAdmin } from './groups/GroupsAdmin';
|
||||||
import { InstanceAdmin } from './instance-admin/InstanceAdmin';
|
import { InstanceAdmin } from './instance-admin/InstanceAdmin';
|
||||||
import { InstancePrivacy } from './instance-privacy/InstancePrivacy';
|
import { InstancePrivacy } from './instance-privacy/InstancePrivacy';
|
||||||
import { MaintenanceAdmin } from './maintenance';
|
import { MaintenanceAdmin } from './maintenance';
|
||||||
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
|
||||||
import { Network } from './network/Network';
|
import { Network } from './network/Network';
|
||||||
import { Roles } from './roles/Roles';
|
import { Roles } from './roles/Roles';
|
||||||
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
|
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
|
||||||
import CreateUser from './users/CreateUser/CreateUser';
|
import CreateUser from './users/CreateUser/CreateUser';
|
||||||
import EditUser from './users/EditUser/EditUser';
|
|
||||||
import { InviteLink } from './users/InviteLink/InviteLink';
|
import { InviteLink } from './users/InviteLink/InviteLink';
|
||||||
import UsersAdmin from './users/UsersAdmin';
|
import UsersAdmin from './users/UsersAdmin';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import NotFound from 'component/common/NotFound/NotFound';
|
||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
import { AdminIndex } from './AdminIndex';
|
||||||
|
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
||||||
|
|
||||||
export const Admin = () => {
|
export const Admin = () => {
|
||||||
const { isEnterprise } = useUiConfig();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminTabsMenu />
|
<AdminTabsMenu />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="users" element={<UsersAdmin />} />
|
<Route index element={<AdminIndex />} />
|
||||||
|
<Route path="users/*" element={<UsersAdmin />} />
|
||||||
<Route path="api" element={<ApiTokenPage />} />
|
<Route path="api" element={<ApiTokenPage />} />
|
||||||
<Route path="api/create-token" element={<CreateApiToken />} />
|
<Route path="api/create-token" element={<CreateApiToken />} />
|
||||||
<Route path="users/:id/edit" element={<EditUser />} />
|
<Route path="service-accounts" element={<ServiceAccounts />} />
|
||||||
<Route
|
|
||||||
path="service-accounts"
|
|
||||||
element={
|
|
||||||
isEnterprise() ? (
|
|
||||||
<ServiceAccounts />
|
|
||||||
) : (
|
|
||||||
<PremiumFeature feature="service-accounts" page />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="create-user" element={<CreateUser />} />
|
<Route path="create-user" element={<CreateUser />} />
|
||||||
<Route path="invite-link" element={<InviteLink />} />
|
<Route path="invite-link" element={<InviteLink />} />
|
||||||
<Route path="groups" element={<GroupsAdmin />} />
|
<Route path="groups/*" element={<GroupsAdmin />} />
|
||||||
<Route path="groups/create-group" element={<CreateGroup />} />
|
<Route path="roles/*" element={<Roles />} />
|
||||||
<Route
|
|
||||||
path="groups/:groupId/edit"
|
|
||||||
element={<EditGroupContainer />}
|
|
||||||
/>
|
|
||||||
<Route path="groups/:groupId" element={<Group />} />
|
|
||||||
<Route
|
|
||||||
path="roles/*"
|
|
||||||
element={
|
|
||||||
isEnterprise() ? (
|
|
||||||
<Roles />
|
|
||||||
) : (
|
|
||||||
<PremiumFeature feature="project-roles" page />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="instance" element={<InstanceAdmin />} />
|
<Route path="instance" element={<InstanceAdmin />} />
|
||||||
<Route path="network/*" element={<Network />} />
|
<Route path="network/*" element={<Network />} />
|
||||||
<Route path="maintenance" element={<MaintenanceAdmin />} />
|
<Route path="maintenance" element={<MaintenanceAdmin />} />
|
||||||
@ -74,6 +44,7 @@ export const Admin = () => {
|
|||||||
/>
|
/>
|
||||||
<Route path="billing" element={<Billing />} />
|
<Route path="billing" element={<Billing />} />
|
||||||
<Route path="instance-privacy" element={<InstancePrivacy />} />
|
<Route path="instance-privacy" element={<InstancePrivacy />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
53
frontend/src/component/admin/AdminIndex.tsx
Normal file
53
frontend/src/component/admin/AdminIndex.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { VFC } from 'react';
|
||||||
|
import { adminGroups } from './adminRoutes';
|
||||||
|
import { INavigationMenuItem } from 'interfaces/route';
|
||||||
|
import { Box, Link, Typography } from '@mui/material';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useAdminRoutes } from './useAdminRoutes';
|
||||||
|
|
||||||
|
export const AdminIndex: VFC = () => {
|
||||||
|
const adminRoutes = useAdminRoutes();
|
||||||
|
|
||||||
|
const routeGroups = adminRoutes.reduce((acc, route) => {
|
||||||
|
const group = route.group || 'other';
|
||||||
|
|
||||||
|
const index = acc.findIndex(item => item.name === group);
|
||||||
|
if (index === -1) {
|
||||||
|
acc.push({
|
||||||
|
name: group,
|
||||||
|
description: adminGroups[group] || 'Other',
|
||||||
|
items: [route],
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[index].items.push(route);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as Array<{ name: string; description: string; items: INavigationMenuItem[] }>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent header={<PageHeader title="Manage Unleash" />}>
|
||||||
|
{routeGroups.map(group => (
|
||||||
|
<Box
|
||||||
|
key={group.name}
|
||||||
|
sx={theme => ({ marginBottom: theme.spacing(2) })}
|
||||||
|
>
|
||||||
|
<Typography variant="h2">{group.description}</Typography>
|
||||||
|
<ul>
|
||||||
|
{group.items.map(route => (
|
||||||
|
<li key={route.path}>
|
||||||
|
<Link component={RouterLink} to={route.path}>
|
||||||
|
{route.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
111
frontend/src/component/admin/adminRoutes.ts
Normal file
111
frontend/src/component/admin/adminRoutes.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { INavigationMenuItem } from 'interfaces/route';
|
||||||
|
|
||||||
|
export const adminGroups: Record<string, string> = {
|
||||||
|
users: 'User administration',
|
||||||
|
access: 'Access control',
|
||||||
|
instance: 'Instance configuration',
|
||||||
|
log: 'Logs',
|
||||||
|
other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adminRoutes: INavigationMenuItem[] = [
|
||||||
|
{
|
||||||
|
path: '/admin/users',
|
||||||
|
title: 'Users',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/service-accounts',
|
||||||
|
title: 'Service accounts',
|
||||||
|
menu: {
|
||||||
|
adminSettings: true,
|
||||||
|
mode: ['enterprise'],
|
||||||
|
},
|
||||||
|
group: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/groups',
|
||||||
|
title: 'Groups',
|
||||||
|
menu: {
|
||||||
|
adminSettings: true,
|
||||||
|
mode: ['enterprise'],
|
||||||
|
},
|
||||||
|
group: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/roles/*',
|
||||||
|
title: 'Roles',
|
||||||
|
menu: {
|
||||||
|
adminSettings: true,
|
||||||
|
mode: ['enterprise'],
|
||||||
|
},
|
||||||
|
group: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/api',
|
||||||
|
title: 'API access',
|
||||||
|
flag: 'frontendNavigationUpdate',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/cors',
|
||||||
|
title: 'CORS origins',
|
||||||
|
flag: 'embedProxyFrontend',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/auth',
|
||||||
|
title: 'Single sign-on',
|
||||||
|
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
|
||||||
|
group: 'access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/network/*',
|
||||||
|
title: 'Network',
|
||||||
|
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
|
||||||
|
configFlag: 'networkViewEnabled',
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/maintenance',
|
||||||
|
title: 'Maintenance',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/instance',
|
||||||
|
title: 'Instance stats',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/instance-privacy',
|
||||||
|
title: 'Instance privacy',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/admin-invoices',
|
||||||
|
title: 'Billing & invoices',
|
||||||
|
menu: { adminSettings: true, mode: ['pro'], billing: true },
|
||||||
|
group: 'instance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/logins',
|
||||||
|
title: 'Login history',
|
||||||
|
menu: {
|
||||||
|
adminSettings: true,
|
||||||
|
mode: ['enterprise'],
|
||||||
|
},
|
||||||
|
group: 'log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/history',
|
||||||
|
title: 'Event log',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
group: 'log',
|
||||||
|
},
|
||||||
|
];
|
@ -1,4 +1,4 @@
|
|||||||
import { Alert } from '@mui/material';
|
import { Alert, Tab, Tabs } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
@ -6,9 +6,11 @@ import { OidcAuth } from './OidcAuth/OidcAuth';
|
|||||||
import { SamlAuth } from './SamlAuth/SamlAuth';
|
import { SamlAuth } from './SamlAuth/SamlAuth';
|
||||||
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
|
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
|
||||||
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
|
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
|
||||||
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
|
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { ADMIN } from '@server/types/permissions';
|
import { ADMIN } from '@server/types/permissions';
|
||||||
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||||
|
|
||||||
export const AuthSettings = () => {
|
export const AuthSettings = () => {
|
||||||
const { authenticationType } = useUiConfig().uiConfig;
|
const { authenticationType } = useUiConfig().uiConfig;
|
||||||
@ -34,24 +36,46 @@ export const AuthSettings = () => {
|
|||||||
].filter(
|
].filter(
|
||||||
item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google'
|
item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google'
|
||||||
);
|
);
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={ADMIN}>
|
||||||
<PageContent header="Single Sign-On">
|
<PageContent
|
||||||
<ConditionallyRender
|
withTabs
|
||||||
condition={authenticationType === 'enterprise'}
|
header={
|
||||||
show={<TabNav tabData={tabs} />}
|
<ConditionallyRender
|
||||||
/>
|
condition={authenticationType === 'enterprise'}
|
||||||
|
show={
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(_, tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
}}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<Tab
|
||||||
|
key={`${tab.label}_${index}`}
|
||||||
|
label={tab.label}
|
||||||
|
id={`tab-${index}`}
|
||||||
|
aria-controls={`tabpanel-${index}`}
|
||||||
|
sx={{
|
||||||
|
minWidth: {
|
||||||
|
lg: 160,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={authenticationType === 'open-source'}
|
condition={authenticationType === 'open-source'}
|
||||||
show={
|
show={<PremiumFeature feature="sso" />}
|
||||||
<Alert severity="warning">
|
|
||||||
You are running the open-source version of
|
|
||||||
Unleash. You have to use the Enterprise edition
|
|
||||||
in order configure Single Sign-on.
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={authenticationType === 'demo'}
|
condition={authenticationType === 'demo'}
|
||||||
@ -83,6 +107,22 @@ export const AuthSettings = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={authenticationType === 'enterprise'}
|
||||||
|
show={
|
||||||
|
<div>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<TabPanel
|
||||||
|
key={index}
|
||||||
|
value={activeTab}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
{tab.component}
|
||||||
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
@ -68,7 +67,7 @@ export const GoogleAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
This integration is deprecated and will be removed in next
|
This integration is deprecated and will be removed in next
|
||||||
@ -240,6 +239,6 @@ export const GoogleAuth = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||||
@ -82,7 +81,7 @@ export const OidcAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<>
|
||||||
<Grid container sx={{ mb: 3 }}>
|
<Grid container sx={{ mb: 3 }}>
|
||||||
<Grid item md={12}>
|
<Grid item md={12}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -292,6 +291,6 @@ export const OidcAuth = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
|
import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
import useAuthSettingsApi, {
|
import useAuthSettingsApi, {
|
||||||
ISimpleAuthSettings,
|
ISimpleAuthSettings,
|
||||||
@ -63,7 +62,7 @@ export const PasswordAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<Alert severity="info" sx={{ mb: 3 }}>
|
<Alert severity="info" sx={{ mb: 3 }}>
|
||||||
Overview of administrators on your Unleash instance:
|
Overview of administrators on your Unleash instance:
|
||||||
@ -134,6 +133,6 @@ export const PasswordAuth = () => {
|
|||||||
tokens={tokens}
|
tokens={tokens}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
@ -73,7 +72,7 @@ export const SamlAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<>
|
||||||
<Grid container sx={{ mb: 3 }}>
|
<Grid container sx={{ mb: 3 }}>
|
||||||
<Grid item md={12}>
|
<Grid item md={12}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -264,6 +263,6 @@ export const SamlAuth = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
</PageContent>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,34 @@
|
|||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import { UG } from 'component/common/flags';
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { GroupsList } from './GroupsList/GroupsList';
|
import { GroupsList } from './GroupsList/GroupsList';
|
||||||
import { ADMIN } from '@server/types/permissions';
|
import { ADMIN } from '@server/types/permissions';
|
||||||
|
import { CreateGroup } from './CreateGroup/CreateGroup';
|
||||||
|
import { EditGroupContainer } from './EditGroup/EditGroup';
|
||||||
|
import { Group } from './Group/Group';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
|
||||||
export const GroupsAdmin = () => (
|
export const GroupsAdmin = () => {
|
||||||
<div>
|
const { uiConfig, isEnterprise } = useUiConfig();
|
||||||
<PermissionGuard permissions={ADMIN}>
|
|
||||||
<GroupsList />
|
if (isEnterprise() || uiConfig.flags[UG] === true) {
|
||||||
</PermissionGuard>
|
return (
|
||||||
</div>
|
<div>
|
||||||
);
|
<PermissionGuard permissions={ADMIN}>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<GroupsList />} />
|
||||||
|
<Route path="create-group" element={<CreateGroup />} />
|
||||||
|
<Route
|
||||||
|
path=":groupId/edit"
|
||||||
|
element={<EditGroupContainer />}
|
||||||
|
/>
|
||||||
|
<Route path=":groupId" element={<Group />} />
|
||||||
|
</Routes>
|
||||||
|
</PermissionGuard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PremiumFeature feature="groups" page />;
|
||||||
|
};
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Paper, styled, Tab, Tabs } from '@mui/material';
|
import { Paper, styled, Tab, Tabs } from '@mui/material';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
|
||||||
import { CenteredNavLink } from './CenteredNavLink';
|
import { CenteredNavLink } from './CenteredNavLink';
|
||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
||||||
|
import { useAdminRoutes } from '../useAdminRoutes';
|
||||||
|
|
||||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
@ -21,88 +21,29 @@ const StyledBadgeContainer = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const AdminTabsMenu: VFC = () => {
|
export const AdminTabsMenu: VFC = () => {
|
||||||
const { uiConfig, isEnterprise, isPro } = useUiConfig();
|
const { uiConfig, isPro, isOss } = useUiConfig();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isBilling } = useInstanceStatus();
|
|
||||||
const { flags, networkViewEnabled } = uiConfig;
|
|
||||||
|
|
||||||
const activeTab = pathname.split('/')[2];
|
const activeTab = pathname.split('/')[2];
|
||||||
|
|
||||||
const showEnterpriseFeaturesInPro =
|
const showEnterpriseFeaturesInPro =
|
||||||
uiConfig?.flags?.frontendNavigationUpdate;
|
uiConfig?.flags?.frontendNavigationUpdate;
|
||||||
|
|
||||||
const tabs = [
|
const adminRoutes = useAdminRoutes();
|
||||||
{
|
const group = adminRoutes.find(route =>
|
||||||
value: 'users',
|
pathname.includes(route.path)
|
||||||
label: 'Users',
|
)?.group;
|
||||||
link: '/admin/users',
|
|
||||||
},
|
const tabs = adminRoutes.filter(
|
||||||
{
|
route =>
|
||||||
value: 'service-accounts',
|
!group ||
|
||||||
label: 'Service accounts',
|
route.group === group ||
|
||||||
link: '/admin/service-accounts',
|
(isOss() && route.group !== 'log')
|
||||||
condition:
|
);
|
||||||
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
|
|
||||||
showEnterpriseBadge: isPro(),
|
if (!group) {
|
||||||
},
|
return null;
|
||||||
{
|
}
|
||||||
value: 'groups',
|
|
||||||
label: 'Groups',
|
|
||||||
link: '/admin/groups',
|
|
||||||
condition: flags.UG,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'roles',
|
|
||||||
label: 'Roles',
|
|
||||||
link: '/admin/roles',
|
|
||||||
condition:
|
|
||||||
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
|
|
||||||
showEnterpriseBadge: isPro(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'api',
|
|
||||||
label: 'API access',
|
|
||||||
link: '/admin/api',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'cors',
|
|
||||||
label: 'CORS origins',
|
|
||||||
link: '/admin/cors',
|
|
||||||
condition: uiConfig.flags.embedProxyFrontend,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'auth',
|
|
||||||
label: 'Single sign-on',
|
|
||||||
link: '/admin/auth',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'instance',
|
|
||||||
label: 'Instance stats',
|
|
||||||
link: '/admin/instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'network',
|
|
||||||
label: 'Network',
|
|
||||||
link: '/admin/network',
|
|
||||||
condition: networkViewEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'maintenance',
|
|
||||||
label: 'Maintenance',
|
|
||||||
link: '/admin/maintenance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'instance-privacy',
|
|
||||||
label: 'Instance privacy',
|
|
||||||
link: '/admin/instance-privacy',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'billing',
|
|
||||||
label: 'Billing',
|
|
||||||
link: '/admin/billing',
|
|
||||||
condition: isBilling,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPaper>
|
<StyledPaper>
|
||||||
@ -112,29 +53,30 @@ export const AdminTabsMenu: VFC = () => {
|
|||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
>
|
>
|
||||||
{tabs
|
{tabs.map(tab => (
|
||||||
.filter(tab => tab.condition || tab.condition === undefined)
|
<Tab
|
||||||
.map(tab => (
|
key={tab.route}
|
||||||
<Tab
|
value={tab.route?.split('/')?.[2]}
|
||||||
key={tab.value}
|
label={
|
||||||
value={tab.value}
|
<CenteredNavLink to={tab.path}>
|
||||||
label={
|
{tab.title}
|
||||||
<CenteredNavLink to={tab.link}>
|
<ConditionallyRender
|
||||||
{tab.label}
|
condition={Boolean(
|
||||||
<ConditionallyRender
|
tab.menu.mode?.includes('enterprise') &&
|
||||||
condition={Boolean(
|
!tab.menu.mode?.includes('pro') &&
|
||||||
tab.showEnterpriseBadge
|
isPro() &&
|
||||||
)}
|
showEnterpriseFeaturesInPro
|
||||||
show={
|
)}
|
||||||
<StyledBadgeContainer>
|
show={
|
||||||
<EnterpriseBadge size={16} />
|
<StyledBadgeContainer>
|
||||||
</StyledBadgeContainer>
|
<EnterpriseBadge size={16} />
|
||||||
}
|
</StyledBadgeContainer>
|
||||||
/>
|
}
|
||||||
</CenteredNavLink>
|
/>
|
||||||
}
|
</CenteredNavLink>
|
||||||
/>
|
}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</StyledPaper>
|
</StyledPaper>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
import { styled, Tab, Tabs } from '@mui/material';
|
import { Tab, Tabs } from '@mui/material';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
@ -8,12 +8,6 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
|
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
|
||||||
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
||||||
|
|
||||||
const StyledPageContent = styled(PageContent)(() => ({
|
|
||||||
'.page-header': {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Overview',
|
label: 'Overview',
|
||||||
@ -30,8 +24,8 @@ export const Network = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StyledPageContent
|
<PageContent
|
||||||
headerClass="page-header"
|
withTabs
|
||||||
header={
|
header={
|
||||||
<Tabs
|
<Tabs
|
||||||
value={pathname}
|
value={pathname}
|
||||||
@ -58,7 +52,7 @@ export const Network = () => {
|
|||||||
<Route path="traffic" element={<NetworkTraffic />} />
|
<Route path="traffic" element={<NetworkTraffic />} />
|
||||||
<Route path="*" element={<NetworkOverview />} />
|
<Route path="*" element={<NetworkOverview />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</StyledPageContent>
|
</PageContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,189 +1,21 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { RolesTable } from './RolesTable/RolesTable';
|
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { Tab, Tabs, styled, useMediaQuery } from '@mui/material';
|
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
|
||||||
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { PROJECT_ROLE_TYPE, ROOT_ROLE_TYPE } from '@server/util/constants';
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import theme from 'themes/theme';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { Add } from '@mui/icons-material';
|
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
||||||
import { IRole } from 'interfaces/role';
|
|
||||||
import { READ_ROLE } from '@server/types/permissions';
|
import { READ_ROLE } from '@server/types/permissions';
|
||||||
|
import { RolesPage } from './RolesPage';
|
||||||
const StyledPageContent = styled(PageContent)(({ theme }) => ({
|
|
||||||
'& .page-header': {
|
|
||||||
padding: theme.spacing(0, 4),
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledTabsContainer = styled('div')({
|
|
||||||
flex: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledActions = styled('div')({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Roles = () => {
|
export const Roles = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
const { pathname } = useLocation();
|
|
||||||
|
|
||||||
const { roles, projectRoles, loading } = useRoles();
|
if (!isEnterprise()) {
|
||||||
|
return <PremiumFeature feature="project-roles" page />;
|
||||||
const [searchValue, setSearchValue] = useState('');
|
}
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
const [selectedRole, setSelectedRole] = useState<IRole>();
|
|
||||||
|
|
||||||
const tabs = uiConfig.flags.customRootRoles
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Root roles',
|
|
||||||
path: '/admin/roles',
|
|
||||||
total: roles.length,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Project roles',
|
|
||||||
path: '/admin/roles/project-roles',
|
|
||||||
total: projectRoles.length,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
label: 'Project roles',
|
|
||||||
path: '/admin/roles',
|
|
||||||
total: projectRoles.length,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
|
|
||||||
const type =
|
|
||||||
!uiConfig.flags.customRootRoles || pathname.includes('project-roles')
|
|
||||||
? PROJECT_ROLE_TYPE
|
|
||||||
: ROOT_ROLE_TYPE;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PermissionGuard permissions={[READ_ROLE, ADMIN]}>
|
<PermissionGuard permissions={[READ_ROLE, ADMIN]}>
|
||||||
<StyledPageContent
|
<RolesPage />
|
||||||
headerClass="page-header"
|
|
||||||
bodyClass="page-body"
|
|
||||||
isLoading={loading}
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<StyledHeader>
|
|
||||||
<StyledTabsContainer>
|
|
||||||
<Tabs
|
|
||||||
value={pathname}
|
|
||||||
indicatorColor="primary"
|
|
||||||
textColor="primary"
|
|
||||||
variant="scrollable"
|
|
||||||
allowScrollButtonsMobile
|
|
||||||
>
|
|
||||||
{tabs.map(({ label, path, total }) => (
|
|
||||||
<Tab
|
|
||||||
key={label}
|
|
||||||
value={path}
|
|
||||||
label={
|
|
||||||
<CenteredNavLink to={path}>
|
|
||||||
<span>
|
|
||||||
{label} ({total})
|
|
||||||
</span>
|
|
||||||
</CenteredNavLink>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</StyledTabsContainer>
|
|
||||||
<StyledActions>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
/>
|
|
||||||
<PageHeader.Divider />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ResponsiveButton
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRole(undefined);
|
|
||||||
setModalOpen(true);
|
|
||||||
}}
|
|
||||||
maxWidth={`${theme.breakpoints.values['sm']}px`}
|
|
||||||
Icon={Add}
|
|
||||||
permission={ADMIN}
|
|
||||||
>
|
|
||||||
New {type} role
|
|
||||||
</ResponsiveButton>
|
|
||||||
</StyledActions>
|
|
||||||
</StyledHeader>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path="project-roles"
|
|
||||||
element={
|
|
||||||
<RolesTable
|
|
||||||
type={PROJECT_ROLE_TYPE}
|
|
||||||
searchValue={searchValue}
|
|
||||||
modalOpen={modalOpen}
|
|
||||||
setModalOpen={setModalOpen}
|
|
||||||
selectedRole={selectedRole}
|
|
||||||
setSelectedRole={setSelectedRole}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
element={
|
|
||||||
<RolesTable
|
|
||||||
type={
|
|
||||||
uiConfig.flags.customRootRoles
|
|
||||||
? ROOT_ROLE_TYPE
|
|
||||||
: PROJECT_ROLE_TYPE
|
|
||||||
}
|
|
||||||
searchValue={searchValue}
|
|
||||||
modalOpen={modalOpen}
|
|
||||||
setModalOpen={setModalOpen}
|
|
||||||
selectedRole={selectedRole}
|
|
||||||
setSelectedRole={setSelectedRole}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</StyledPageContent>
|
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
175
frontend/src/component/admin/roles/RolesPage.tsx
Normal file
175
frontend/src/component/admin/roles/RolesPage.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { RolesTable } from './RolesTable/RolesTable';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { Tab, Tabs, styled, useMediaQuery } from '@mui/material';
|
||||||
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { PROJECT_ROLE_TYPE, ROOT_ROLE_TYPE } from '@server/util/constants';
|
||||||
|
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { IRole } from 'interfaces/role';
|
||||||
|
|
||||||
|
const StyledHeader = styled('div')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTabsContainer = styled('div')({
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledActions = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RolesPage = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const { roles, projectRoles, loading } = useRoles();
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [selectedRole, setSelectedRole] = useState<IRole>();
|
||||||
|
|
||||||
|
const tabs = uiConfig.flags.customRootRoles
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Root roles',
|
||||||
|
path: '/admin/roles',
|
||||||
|
total: roles.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project roles',
|
||||||
|
path: '/admin/roles/project-roles',
|
||||||
|
total: projectRoles.length,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: 'Project roles',
|
||||||
|
path: '/admin/roles',
|
||||||
|
total: projectRoles.length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const type =
|
||||||
|
!uiConfig.flags.customRootRoles || pathname.includes('project-roles')
|
||||||
|
? PROJECT_ROLE_TYPE
|
||||||
|
: ROOT_ROLE_TYPE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
withTabs
|
||||||
|
bodyClass="page-body"
|
||||||
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<>
|
||||||
|
<StyledHeader>
|
||||||
|
<StyledTabsContainer>
|
||||||
|
<Tabs
|
||||||
|
value={pathname}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
variant="scrollable"
|
||||||
|
allowScrollButtonsMobile
|
||||||
|
>
|
||||||
|
{tabs.map(({ label, path, total }) => (
|
||||||
|
<Tab
|
||||||
|
key={label}
|
||||||
|
value={path}
|
||||||
|
label={
|
||||||
|
<CenteredNavLink to={path}>
|
||||||
|
<span>
|
||||||
|
{label} ({total})
|
||||||
|
</span>
|
||||||
|
</CenteredNavLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</StyledTabsContainer>
|
||||||
|
<StyledActions>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
<PageHeader.Divider />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ResponsiveButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRole(undefined);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
maxWidth={`${theme.breakpoints.values['sm']}px`}
|
||||||
|
Icon={Add}
|
||||||
|
permission={ADMIN}
|
||||||
|
>
|
||||||
|
New {type} role
|
||||||
|
</ResponsiveButton>
|
||||||
|
</StyledActions>
|
||||||
|
</StyledHeader>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="project-roles"
|
||||||
|
element={
|
||||||
|
<RolesTable
|
||||||
|
type={PROJECT_ROLE_TYPE}
|
||||||
|
searchValue={searchValue}
|
||||||
|
modalOpen={modalOpen}
|
||||||
|
setModalOpen={setModalOpen}
|
||||||
|
selectedRole={selectedRole}
|
||||||
|
setSelectedRole={setSelectedRole}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<RolesTable
|
||||||
|
type={
|
||||||
|
uiConfig.flags.customRootRoles
|
||||||
|
? ROOT_ROLE_TYPE
|
||||||
|
: PROJECT_ROLE_TYPE
|
||||||
|
}
|
||||||
|
searchValue={searchValue}
|
||||||
|
modalOpen={modalOpen}
|
||||||
|
setModalOpen={setModalOpen}
|
||||||
|
selectedRole={selectedRole}
|
||||||
|
setSelectedRole={setSelectedRole}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -1,11 +1,25 @@
|
|||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { ServiceAccountsTable } from './ServiceAccountsTable/ServiceAccountsTable';
|
import { ServiceAccountsTable } from './ServiceAccountsTable/ServiceAccountsTable';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
import { AdminTabsMenu } from '../menu/AdminTabsMenu';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
export const ServiceAccounts = () => (
|
export const ServiceAccounts = () => {
|
||||||
<div>
|
const { isEnterprise } = useUiConfig();
|
||||||
<PermissionGuard permissions={ADMIN}>
|
|
||||||
<ServiceAccountsTable />
|
return (
|
||||||
</PermissionGuard>
|
<div>
|
||||||
</div>
|
<ConditionallyRender
|
||||||
);
|
condition={isEnterprise()}
|
||||||
|
show={
|
||||||
|
<PermissionGuard permissions={ADMIN}>
|
||||||
|
<ServiceAccountsTable />
|
||||||
|
</PermissionGuard>
|
||||||
|
}
|
||||||
|
elseShow={<PremiumFeature feature="service-accounts" page />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
28
frontend/src/component/admin/useAdminRoutes.ts
Normal file
28
frontend/src/component/admin/useAdminRoutes.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { adminRoutes } from './adminRoutes';
|
||||||
|
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||||
|
import { filterAdminRoutes } from './filterAdminRoutes';
|
||||||
|
import { filterByConfig, mapRouteLink } from 'component/common/util';
|
||||||
|
|
||||||
|
export const useAdminRoutes = () => {
|
||||||
|
const { uiConfig, isPro, isEnterprise } = useUiConfig();
|
||||||
|
const { isBilling } = useInstanceStatus();
|
||||||
|
const showEnterpriseOptionsInPro = Boolean(
|
||||||
|
uiConfig?.flags?.frontendNavigationUpdate
|
||||||
|
);
|
||||||
|
|
||||||
|
return adminRoutes
|
||||||
|
.filter(filterByConfig(uiConfig))
|
||||||
|
.filter(route =>
|
||||||
|
filterAdminRoutes(
|
||||||
|
route?.menu,
|
||||||
|
{
|
||||||
|
enterprise: isEnterprise(),
|
||||||
|
pro: isPro(),
|
||||||
|
billing: isBilling,
|
||||||
|
},
|
||||||
|
showEnterpriseOptionsInPro
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(mapRouteLink);
|
||||||
|
};
|
@ -2,12 +2,26 @@ import UsersList from './UsersList/UsersList';
|
|||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { InviteLinkBar } from './InviteLinkBar/InviteLinkBar';
|
import { InviteLinkBar } from './InviteLinkBar/InviteLinkBar';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import EditUser from './EditUser/EditUser';
|
||||||
|
import NotFound from 'component/common/NotFound/NotFound';
|
||||||
|
|
||||||
export const UsersAdmin = () => (
|
export const UsersAdmin = () => (
|
||||||
<div>
|
<div>
|
||||||
<InviteLinkBar />
|
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={ADMIN}>
|
||||||
<UsersList />
|
<Routes>
|
||||||
|
<Route
|
||||||
|
index
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<InviteLinkBar />
|
||||||
|
<UsersList />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path=":id/edit" element={<EditUser />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
/* eslint react/no-multi-comp:off */
|
/* eslint react/no-multi-comp:off */
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Avatar,
|
Avatar,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Link,
|
Link,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as LinkIcon } from '@mui/icons-material';
|
import { Link as LinkIcon } from '@mui/icons-material';
|
||||||
@ -13,7 +16,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ApplicationView } from '../ApplicationView/ApplicationView';
|
import { ApplicationView } from '../ApplicationView/ApplicationView';
|
||||||
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
|
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
|
||||||
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
|
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
@ -27,6 +29,7 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton
|
|||||||
import { formatDateYMD } from 'utils/formatDate';
|
import { formatDateYMD } from 'utils/formatDate';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||||
|
|
||||||
export const ApplicationEdit = () => {
|
export const ApplicationEdit = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -37,6 +40,7 @@ export const ApplicationEdit = () => {
|
|||||||
const { deleteApplication } = useApplicationsApi();
|
const { deleteApplication } = useApplicationsApi();
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
@ -91,8 +95,8 @@ export const ApplicationEdit = () => {
|
|||||||
return <p>Application ({appName}) not found</p>;
|
return <p>Application ({appName}) not found</p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<>
|
||||||
header={
|
<PageContent>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
titleElement={
|
titleElement={
|
||||||
<span
|
<span
|
||||||
@ -133,23 +137,59 @@ export const ApplicationEdit = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
<Box sx={theme => ({ marginTop: theme.spacing(1) })}>
|
||||||
>
|
<Typography variant="body1">{description || ''}</Typography>
|
||||||
<div>
|
<Typography variant="body2">
|
||||||
<Typography variant="body1">{description || ''}</Typography>
|
Created: <strong>{formatDate(createdAt)}</strong>
|
||||||
<Typography variant="body2">
|
</Typography>
|
||||||
Created: <strong>{formatDate(createdAt)}</strong>
|
</Box>
|
||||||
</Typography>
|
</PageContent>
|
||||||
</div>
|
<br />
|
||||||
<ConditionallyRender
|
<PageContent
|
||||||
condition={hasAccess(UPDATE_APPLICATION)}
|
withTabs
|
||||||
show={
|
header={
|
||||||
<div>
|
<Tabs
|
||||||
{renderModal()}
|
value={activeTab}
|
||||||
<TabNav tabData={tabData} />
|
onChange={(_, tabId) => {
|
||||||
</div>
|
setActiveTab(tabId);
|
||||||
|
}}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
{tabData.map((tab, index) => (
|
||||||
|
<Tab
|
||||||
|
key={`${tab.label}_${index}`}
|
||||||
|
label={tab.label}
|
||||||
|
id={`tab-${index}`}
|
||||||
|
aria-controls={`tabpanel-${index}`}
|
||||||
|
sx={{
|
||||||
|
minWidth: {
|
||||||
|
lg: 160,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
</PageContent>
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(UPDATE_APPLICATION)}
|
||||||
|
show={
|
||||||
|
<div>
|
||||||
|
{renderModal()}
|
||||||
|
{tabData.map((tab, index) => (
|
||||||
|
<TabPanel
|
||||||
|
key={index}
|
||||||
|
value={activeTab}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
{tab.component}
|
||||||
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -102,6 +102,7 @@ export const ApplicationView = () => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container style={{ margin: 0 }}>
|
<Grid container style={{ margin: 0 }}>
|
||||||
<Grid item xl={6} md={6} xs={12}>
|
<Grid item xl={6} md={6} xs={12}>
|
||||||
|
@ -83,14 +83,6 @@ const BreadcrumbNav = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (index === 0 && path === 'admin') {
|
|
||||||
return (
|
|
||||||
<StyledParagraph key={path}>
|
|
||||||
{path}
|
|
||||||
</StyledParagraph>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLink key={path} to={link}>
|
<StyledLink key={path} to={link}>
|
||||||
<StyledParagraph>
|
<StyledParagraph>
|
||||||
|
@ -9,7 +9,19 @@ type EnterpriseBadgeProps = {
|
|||||||
|
|
||||||
export const EnterpriseBadge: VFC<EnterpriseBadgeProps> = ({ size = 16 }) => (
|
export const EnterpriseBadge: VFC<EnterpriseBadgeProps> = ({ size = 16 }) => (
|
||||||
<ThemeMode
|
<ThemeMode
|
||||||
darkmode={<ProPlanIconLight width={size} height={size} />}
|
darkmode={
|
||||||
lightmode={<ProPlanIcon width={size} height={size} />}
|
<ProPlanIconLight
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ filter: 'grayscale(100%)', opacity: 0.51 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
lightmode={
|
||||||
|
<ProPlanIcon
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ filter: 'grayscale(100%)', opacity: 0.6 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,12 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
headerPadding: {
|
headerPadding: {
|
||||||
padding: theme.spacing(2, 4),
|
padding: theme.spacing(2, 4),
|
||||||
},
|
},
|
||||||
|
withTabs: {
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
bodyContainer: {
|
bodyContainer: {
|
||||||
padding: theme.spacing(4),
|
padding: theme.spacing(4),
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
|
@ -20,6 +20,7 @@ interface IPageContentProps extends PaperProps {
|
|||||||
disableLoading?: boolean;
|
disableLoading?: boolean;
|
||||||
bodyClass?: string;
|
bodyClass?: string;
|
||||||
headerClass?: string;
|
headerClass?: string;
|
||||||
|
withTabs?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
@ -59,6 +60,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
disableLoading = false,
|
disableLoading = false,
|
||||||
className,
|
className,
|
||||||
|
withTabs,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
@ -69,6 +71,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
|||||||
{
|
{
|
||||||
[styles.paddingDisabled]: disablePadding,
|
[styles.paddingDisabled]: disablePadding,
|
||||||
[styles.borderDisabled]: disableBorder,
|
[styles.borderDisabled]: disableBorder,
|
||||||
|
[styles.withTabs]: withTabs,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -38,8 +38,9 @@ const StyledTypography = styled(Typography)(({ theme }) => ({
|
|||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(() => ({
|
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
@ -87,6 +88,16 @@ const PremiumFeatures = {
|
|||||||
url: 'https://docs.getunleash.io/reference/login-history',
|
url: 'https://docs.getunleash.io/reference/login-history',
|
||||||
label: 'Login history',
|
label: 'Login history',
|
||||||
},
|
},
|
||||||
|
groups: {
|
||||||
|
plan: FeaturePlan.ENTERPRISE,
|
||||||
|
url: 'https://docs.getunleash.io/reference/rbac#user-groups',
|
||||||
|
label: 'User groups',
|
||||||
|
},
|
||||||
|
sso: {
|
||||||
|
plan: FeaturePlan.PRO,
|
||||||
|
url: 'https://docs.getunleash.io/reference/rbac#user-group-sso-integration',
|
||||||
|
label: 'Single Sign-On',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type PremiumFeatureType = keyof typeof PremiumFeatures;
|
type PremiumFeatureType = keyof typeof PremiumFeatures;
|
||||||
@ -126,7 +137,7 @@ export const PremiumFeature = ({
|
|||||||
<>
|
<>
|
||||||
{featureLabel} is a feature available for the{' '}
|
{featureLabel} is a feature available for the{' '}
|
||||||
<strong>{plan}</strong>{' '}
|
<strong>{plan}</strong>{' '}
|
||||||
{plan === FeaturePlan.PRO ? 'plans' : 'plan'}
|
{plan === FeaturePlan.PRO ? 'plans' : 'plan'}.
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -148,7 +159,7 @@ export const PremiumFeature = ({
|
|||||||
<StyledBody tooltip>
|
<StyledBody tooltip>
|
||||||
<StyledTypography>
|
<StyledTypography>
|
||||||
{featureMessage}. You need to upgrade your plan
|
{featureMessage}. You need to upgrade your plan
|
||||||
if you want to use it
|
if you want to use it.
|
||||||
</StyledTypography>
|
</StyledTypography>
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
@ -158,7 +169,7 @@ export const PremiumFeature = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
Upgrade now
|
Compare plans
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</>
|
</>
|
||||||
@ -171,18 +182,26 @@ export const PremiumFeature = ({
|
|||||||
</StyledTypography>
|
</StyledTypography>
|
||||||
<StyledTypography>
|
<StyledTypography>
|
||||||
You need to upgrade your plan if you want to use
|
You need to upgrade your plan if you want to use
|
||||||
it
|
it.
|
||||||
</StyledTypography>
|
</StyledTypography>
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="contained"
|
||||||
href={upgradeUrl}
|
href={upgradeUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
Upgrade now
|
Compare plans
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Read about {label}
|
||||||
</Button>
|
</Button>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</>
|
</>
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import React, { useState, ReactNode } from 'react';
|
|
||||||
import { Tabs, Tab, Paper } from '@mui/material';
|
|
||||||
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
|
||||||
|
|
||||||
interface ITabNavProps {
|
|
||||||
tabData: ITabData[];
|
|
||||||
className?: string;
|
|
||||||
navClass?: string;
|
|
||||||
startingTab?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ITabData {
|
|
||||||
label: string;
|
|
||||||
component: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TabNav = ({
|
|
||||||
tabData,
|
|
||||||
className = '',
|
|
||||||
navClass = '',
|
|
||||||
startingTab = 0,
|
|
||||||
}: ITabNavProps) => {
|
|
||||||
const [activeTab, setActiveTab] = useState(startingTab);
|
|
||||||
const renderTabs = () =>
|
|
||||||
tabData.map((tab, index) => (
|
|
||||||
<Tab
|
|
||||||
key={`${tab.label}_${index}`}
|
|
||||||
label={tab.label}
|
|
||||||
id={`tab-${index}`}
|
|
||||||
aria-controls={`tabpanel-${index}`}
|
|
||||||
sx={{
|
|
||||||
minWidth: {
|
|
||||||
lg: 160,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
const renderTabPanels = () =>
|
|
||||||
tabData.map((tab, index) => (
|
|
||||||
<TabPanel key={index} value={activeTab} index={index}>
|
|
||||||
{tab.component}
|
|
||||||
</TabPanel>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Paper
|
|
||||||
className={navClass}
|
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme => theme.palette.background.paper,
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderBottomColor: theme => theme.palette.divider,
|
|
||||||
borderRadius: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(_, tabId) => {
|
|
||||||
setActiveTab(tabId);
|
|
||||||
}}
|
|
||||||
indicatorColor="primary"
|
|
||||||
textColor="primary"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
{renderTabs()}
|
|
||||||
</Tabs>
|
|
||||||
</Paper>
|
|
||||||
<div className={className}>{renderTabPanels()}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface ITabPanelProps {
|
interface ITabPanelProps {
|
||||||
value: number;
|
value: number;
|
||||||
|
@ -5,6 +5,9 @@ import { IFeatureVariant } from 'interfaces/featureToggle';
|
|||||||
import { format, isValid } from 'date-fns';
|
import { format, isValid } from 'date-fns';
|
||||||
import { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
|
import { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle feature flags and configuration for different plans.
|
||||||
|
*/
|
||||||
export const filterByConfig =
|
export const filterByConfig =
|
||||||
(config: IUiConfig) => (r: INavigationMenuItem) => {
|
(config: IUiConfig) => (r: INavigationMenuItem) => {
|
||||||
if (r.flag) {
|
if (r.flag) {
|
||||||
@ -25,6 +28,12 @@ export const scrollToTop = () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mapRouteLink = (route: INavigationMenuItem) => ({
|
||||||
|
...route,
|
||||||
|
path: route.path.replace('/*', ''),
|
||||||
|
route: route.path,
|
||||||
|
});
|
||||||
|
|
||||||
export const trim = (value: string): string => {
|
export const trim = (value: string): string => {
|
||||||
if (value && value.trim) {
|
if (value && value.trim) {
|
||||||
return value.trim();
|
return value.trim();
|
||||||
|
@ -22,24 +22,18 @@ import { DrawerMenu } from './DrawerMenu/DrawerMenu';
|
|||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { flexRow, focusable } from 'themes/themeStyles';
|
import { flexRow, focusable } from 'themes/themeStyles';
|
||||||
import { NavigationMenu } from './NavigationMenu/NavigationMenu';
|
import { NavigationMenu } from './NavigationMenu/NavigationMenu';
|
||||||
import {
|
import { getRoutes, getCondensedRoutes } from 'component/menu/routes';
|
||||||
getRoutes,
|
|
||||||
adminMenuRoutes,
|
|
||||||
getCondensedRoutes,
|
|
||||||
} from 'component/menu/routes';
|
|
||||||
import {
|
import {
|
||||||
DarkModeOutlined,
|
DarkModeOutlined,
|
||||||
KeyboardArrowDown,
|
KeyboardArrowDown,
|
||||||
LightModeOutlined,
|
LightModeOutlined,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { filterByConfig } from 'component/common/util';
|
import { filterByConfig, mapRouteLink } from 'component/common/util';
|
||||||
import { useId } from 'hooks/useId';
|
import { useId } from 'hooks/useId';
|
||||||
import { INavigationMenuItem } from 'interfaces/route';
|
|
||||||
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
|
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
|
||||||
import { useThemeMode } from 'hooks/useThemeMode';
|
import { useThemeMode } from 'hooks/useThemeMode';
|
||||||
import { Notifications } from 'component/common/Notifications/Notifications';
|
import { Notifications } from 'component/common/Notifications/Notifications';
|
||||||
import { filterAdminRoutes } from './filterAdminRoutes';
|
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
|
||||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
|
||||||
|
|
||||||
const StyledHeader = styled(AppBar)(({ theme }) => ({
|
const StyledHeader = styled(AppBar)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
@ -109,11 +103,6 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
|||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mapRouteLink = (route: INavigationMenuItem) => ({
|
|
||||||
...route,
|
|
||||||
path: route.path.replace('/*', ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
const Header: VFC = () => {
|
const Header: VFC = () => {
|
||||||
const { onSetThemeMode, themeMode } = useThemeMode();
|
const { onSetThemeMode, themeMode } = useThemeMode();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -122,20 +111,17 @@ const Header: VFC = () => {
|
|||||||
const [adminRef, setAdminRef] = useState<HTMLButtonElement | null>(null);
|
const [adminRef, setAdminRef] = useState<HTMLButtonElement | null>(null);
|
||||||
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
|
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const { uiConfig, isOss, isPro, isEnterprise } = useUiConfig();
|
const { uiConfig, isOss } = useUiConfig();
|
||||||
const { isBilling } = useInstanceStatus();
|
|
||||||
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
const showApiAccessInConfigure = !uiConfig?.flags?.frontendNavigationUpdate;
|
const showApiAccessInConfigure = !uiConfig?.flags?.frontendNavigationUpdate;
|
||||||
const showEnterpriseOptionsInPro = Boolean(
|
|
||||||
uiConfig?.flags?.frontendNavigationUpdate
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
||||||
const onAdminClose = () => setAdminRef(null);
|
const onAdminClose = () => setAdminRef(null);
|
||||||
const onConfigureClose = () => setConfigRef(null);
|
const onConfigureClose = () => setConfigRef(null);
|
||||||
|
|
||||||
const routes = getRoutes();
|
const routes = getRoutes();
|
||||||
|
const adminRoutes = useAdminRoutes();
|
||||||
|
|
||||||
const filteredMainRoutes = {
|
const filteredMainRoutes = {
|
||||||
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
|
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
|
||||||
@ -166,20 +152,7 @@ const Header: VFC = () => {
|
|||||||
)
|
)
|
||||||
.filter(filterByConfig(uiConfig))
|
.filter(filterByConfig(uiConfig))
|
||||||
.map(mapRouteLink),
|
.map(mapRouteLink),
|
||||||
adminRoutes: adminMenuRoutes
|
adminRoutes,
|
||||||
.filter(filterByConfig(uiConfig))
|
|
||||||
.filter(route =>
|
|
||||||
filterAdminRoutes(
|
|
||||||
route?.menu,
|
|
||||||
{
|
|
||||||
enterprise: isEnterprise(),
|
|
||||||
pro: isPro(),
|
|
||||||
billing: isBilling,
|
|
||||||
},
|
|
||||||
showEnterpriseOptionsInPro
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map(mapRouteLink),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (smallScreen) {
|
if (smallScreen) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Divider } from '@mui/material';
|
import { Divider, Tooltip } from '@mui/material';
|
||||||
import { Menu, MenuItem, styled } from '@mui/material';
|
import { Menu, MenuItem, styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
@ -50,7 +50,7 @@ export const NavigationMenu = ({
|
|||||||
anchorEl,
|
anchorEl,
|
||||||
style,
|
style,
|
||||||
}: INavigationMenuProps) => {
|
}: INavigationMenuProps) => {
|
||||||
const { uiConfig, isPro } = useUiConfig();
|
const { uiConfig, isPro, isOss } = useUiConfig();
|
||||||
const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate;
|
const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate;
|
||||||
|
|
||||||
const showBadge = useCallback(
|
const showBadge = useCallback(
|
||||||
@ -83,29 +83,40 @@ export const NavigationMenu = ({
|
|||||||
const addDivider =
|
const addDivider =
|
||||||
showUpdatedMenu &&
|
showUpdatedMenu &&
|
||||||
previousGroup &&
|
previousGroup &&
|
||||||
previousGroup !== option.group;
|
previousGroup !== option.group &&
|
||||||
|
(!isOss() || option.group === 'log');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
addDivider ? (
|
addDivider ? (
|
||||||
<Divider variant="middle" key={option.group} />
|
<Divider variant="middle" key={option.group} />
|
||||||
) : null,
|
) : null,
|
||||||
<MenuItem
|
<Tooltip
|
||||||
key={option.path}
|
title={
|
||||||
component={StyledLink}
|
showBadge(option?.menu?.mode)
|
||||||
to={option.path}
|
? 'This is an Enterprise feature'
|
||||||
onClick={handleClose}
|
: ''
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
placement="left"
|
||||||
>
|
>
|
||||||
<StyledSpan />
|
<MenuItem
|
||||||
{option.title}
|
key={option.path}
|
||||||
<ConditionallyRender
|
component={StyledLink}
|
||||||
condition={showBadge(option?.menu?.mode)}
|
to={option.path}
|
||||||
show={
|
onClick={handleClose}
|
||||||
<StyledBadgeContainer>
|
>
|
||||||
<EnterpriseBadge />
|
<StyledSpan />
|
||||||
</StyledBadgeContainer>
|
{option.title}
|
||||||
}
|
<ConditionallyRender
|
||||||
/>
|
condition={showBadge(option?.menu?.mode)}
|
||||||
</MenuItem>,
|
show={
|
||||||
|
<StyledBadgeContainer>
|
||||||
|
<EnterpriseBadge />
|
||||||
|
</StyledBadgeContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
</Tooltip>,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.flat()
|
.flat()
|
||||||
|
@ -374,14 +374,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Archived toggles",
|
"title": "Archived toggles",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"hidden": false,
|
|
||||||
"menu": {},
|
|
||||||
"path": "/admin",
|
|
||||||
"title": "Admin",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
|
@ -4,7 +4,7 @@ import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesLi
|
|||||||
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
||||||
import { AddonList } from 'component/addons/AddonList/AddonList';
|
import { AddonList } from 'component/addons/AddonList/AddonList';
|
||||||
import Login from 'component/user/Login/Login';
|
import Login from 'component/user/Login/Login';
|
||||||
import { EEA, P, SE, UG } from 'component/common/flags';
|
import { EEA, P, SE } from 'component/common/flags';
|
||||||
import { NewUser } from 'component/user/NewUser/NewUser';
|
import { NewUser } from 'component/user/NewUser/NewUser';
|
||||||
import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
||||||
@ -42,7 +42,6 @@ import { LazyCreateProject } from 'component/project/Project/CreateProject/LazyC
|
|||||||
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
||||||
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
|
||||||
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||||
|
|
||||||
@ -385,15 +384,6 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
title: 'Admin',
|
|
||||||
component: AdminRedirect,
|
|
||||||
hidden: false,
|
|
||||||
type: 'protected',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/admin/*',
|
path: '/admin/*',
|
||||||
title: 'Admin',
|
title: 'Admin',
|
||||||
@ -452,109 +442,6 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const adminMenuRoutes: INavigationMenuItem[] = [
|
|
||||||
{
|
|
||||||
path: '/admin/users',
|
|
||||||
title: 'Users',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'users',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/service-accounts',
|
|
||||||
title: 'Service accounts',
|
|
||||||
menu: {
|
|
||||||
adminSettings: true,
|
|
||||||
mode: ['enterprise'],
|
|
||||||
},
|
|
||||||
group: 'users',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/groups',
|
|
||||||
title: 'Groups',
|
|
||||||
menu: {
|
|
||||||
adminSettings: true,
|
|
||||||
mode: ['enterprise'],
|
|
||||||
},
|
|
||||||
flag: UG,
|
|
||||||
group: 'users',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/roles/*',
|
|
||||||
title: 'Roles',
|
|
||||||
menu: {
|
|
||||||
adminSettings: true,
|
|
||||||
mode: ['enterprise'],
|
|
||||||
},
|
|
||||||
group: 'users',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/api',
|
|
||||||
title: 'API access',
|
|
||||||
flag: 'frontendNavigationUpdate',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'access',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/cors',
|
|
||||||
title: 'CORS origins',
|
|
||||||
flag: 'embedProxyFrontend',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'access',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/auth',
|
|
||||||
title: 'Single sign-on',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'access',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/network/*',
|
|
||||||
title: 'Network',
|
|
||||||
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
|
|
||||||
configFlag: 'networkViewEnabled',
|
|
||||||
group: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/maintenance',
|
|
||||||
title: 'Maintenance',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/instance',
|
|
||||||
title: 'Instance stats',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/instance-privacy',
|
|
||||||
title: 'Instance privacy',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/admin-invoices',
|
|
||||||
title: 'Billing & invoices',
|
|
||||||
menu: { adminSettings: true, mode: ['pro'], billing: true },
|
|
||||||
group: 'instance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/logins',
|
|
||||||
title: 'Login history',
|
|
||||||
menu: {
|
|
||||||
adminSettings: true,
|
|
||||||
mode: ['enterprise'],
|
|
||||||
},
|
|
||||||
group: 'log',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/history',
|
|
||||||
title: 'Event log',
|
|
||||||
menu: { adminSettings: true },
|
|
||||||
group: 'log',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getRoute = (path: string) =>
|
export const getRoute = (path: string) =>
|
||||||
routes.find(route => route.path === path);
|
routes.find(route => route.path === path);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user