mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Update admin navigation (1-1104-improved-menu-oss) (#4458)
Cleaner tabs navigation for admin tabs ![image](https://github.com/Unleash/unleash/assets/2625371/1858276b-543f-42e3-85be-385090558a03) 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 FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
||||
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 { InstanceAdmin } from './instance-admin/InstanceAdmin';
|
||||
import { InstancePrivacy } from './instance-privacy/InstancePrivacy';
|
||||
import { MaintenanceAdmin } from './maintenance';
|
||||
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
||||
import { Network } from './network/Network';
|
||||
import { Roles } from './roles/Roles';
|
||||
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
|
||||
import CreateUser from './users/CreateUser/CreateUser';
|
||||
import EditUser from './users/EditUser/EditUser';
|
||||
import { InviteLink } from './users/InviteLink/InviteLink';
|
||||
import UsersAdmin from './users/UsersAdmin';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import NotFound from 'component/common/NotFound/NotFound';
|
||||
import { AdminIndex } from './AdminIndex';
|
||||
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
||||
|
||||
export const Admin = () => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminTabsMenu />
|
||||
<Routes>
|
||||
<Route path="users" element={<UsersAdmin />} />
|
||||
<Route index element={<AdminIndex />} />
|
||||
<Route path="users/*" element={<UsersAdmin />} />
|
||||
<Route path="api" element={<ApiTokenPage />} />
|
||||
<Route path="api/create-token" element={<CreateApiToken />} />
|
||||
<Route path="users/:id/edit" element={<EditUser />} />
|
||||
<Route
|
||||
path="service-accounts"
|
||||
element={
|
||||
isEnterprise() ? (
|
||||
<ServiceAccounts />
|
||||
) : (
|
||||
<PremiumFeature feature="service-accounts" page />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route path="service-accounts" element={<ServiceAccounts />} />
|
||||
<Route path="create-user" element={<CreateUser />} />
|
||||
<Route path="invite-link" element={<InviteLink />} />
|
||||
<Route path="groups" element={<GroupsAdmin />} />
|
||||
<Route path="groups/create-group" element={<CreateGroup />} />
|
||||
<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="groups/*" element={<GroupsAdmin />} />
|
||||
<Route path="roles/*" element={<Roles />} />
|
||||
<Route path="instance" element={<InstanceAdmin />} />
|
||||
<Route path="network/*" element={<Network />} />
|
||||
<Route path="maintenance" element={<MaintenanceAdmin />} />
|
||||
@ -74,6 +44,7 @@ export const Admin = () => {
|
||||
/>
|
||||
<Route path="billing" element={<Billing />} />
|
||||
<Route path="instance-privacy" element={<InstancePrivacy />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -6,9 +6,11 @@ import { OidcAuth } from './OidcAuth/OidcAuth';
|
||||
import { SamlAuth } from './SamlAuth/SamlAuth';
|
||||
import { PasswordAuth } from './PasswordAuth/PasswordAuth';
|
||||
import { GoogleAuth } from './GoogleAuth/GoogleAuth';
|
||||
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
|
||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||
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 = () => {
|
||||
const { authenticationType } = useUiConfig().uiConfig;
|
||||
@ -34,24 +36,46 @@ export const AuthSettings = () => {
|
||||
].filter(
|
||||
item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google'
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<PageContent header="Single Sign-On">
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'enterprise'}
|
||||
show={<TabNav tabData={tabs} />}
|
||||
/>
|
||||
<PageContent
|
||||
withTabs
|
||||
header={
|
||||
<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
|
||||
condition={authenticationType === 'open-source'}
|
||||
show={
|
||||
<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>
|
||||
}
|
||||
show={<PremiumFeature feature="sso" />}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'demo'}
|
||||
@ -83,6 +107,22 @@ export const AuthSettings = () => {
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'enterprise'}
|
||||
show={
|
||||
<div>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabPanel
|
||||
key={index}
|
||||
value={activeTab}
|
||||
index={index}
|
||||
>
|
||||
{tab.component}
|
||||
</TabPanel>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||
@ -68,7 +67,7 @@ export const GoogleAuth = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<>
|
||||
<Box>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
This integration is deprecated and will be removed in next
|
||||
@ -240,6 +239,6 @@ export const GoogleAuth = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
|
||||
@ -82,7 +81,7 @@ export const OidcAuth = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
<Grid item md={12}>
|
||||
<Alert severity="info">
|
||||
@ -292,6 +291,6 @@ export const OidcAuth = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||
import useAuthSettingsApi, {
|
||||
ISimpleAuthSettings,
|
||||
@ -63,7 +62,7 @@ export const PasswordAuth = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
Overview of administrators on your Unleash instance:
|
||||
@ -134,6 +133,6 @@ export const PasswordAuth = () => {
|
||||
tokens={tokens}
|
||||
/>
|
||||
</form>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
|
||||
import useToast from 'hooks/useToast';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -73,7 +72,7 @@ export const SamlAuth = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
<Grid item md={12}>
|
||||
<Alert severity="info">
|
||||
@ -264,6 +263,6 @@ export const SamlAuth = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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 { GroupsList } from './GroupsList/GroupsList';
|
||||
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 = () => (
|
||||
<div>
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<GroupsList />
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
);
|
||||
export const GroupsAdmin = () => {
|
||||
const { uiConfig, isEnterprise } = useUiConfig();
|
||||
|
||||
if (isEnterprise() || uiConfig.flags[UG] === true) {
|
||||
return (
|
||||
<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 { Paper, styled, Tab, Tabs } from '@mui/material';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
import { CenteredNavLink } from './CenteredNavLink';
|
||||
import { VFC } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
||||
import { useAdminRoutes } from '../useAdminRoutes';
|
||||
|
||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
marginBottom: '1rem',
|
||||
@ -21,88 +21,29 @@ const StyledBadgeContainer = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export const AdminTabsMenu: VFC = () => {
|
||||
const { uiConfig, isEnterprise, isPro } = useUiConfig();
|
||||
const { uiConfig, isPro, isOss } = useUiConfig();
|
||||
const { pathname } = useLocation();
|
||||
const { isBilling } = useInstanceStatus();
|
||||
const { flags, networkViewEnabled } = uiConfig;
|
||||
|
||||
const activeTab = pathname.split('/')[2];
|
||||
|
||||
const showEnterpriseFeaturesInPro =
|
||||
uiConfig?.flags?.frontendNavigationUpdate;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
value: 'users',
|
||||
label: 'Users',
|
||||
link: '/admin/users',
|
||||
},
|
||||
{
|
||||
value: 'service-accounts',
|
||||
label: 'Service accounts',
|
||||
link: '/admin/service-accounts',
|
||||
condition:
|
||||
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
|
||||
showEnterpriseBadge: isPro(),
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
const adminRoutes = useAdminRoutes();
|
||||
const group = adminRoutes.find(route =>
|
||||
pathname.includes(route.path)
|
||||
)?.group;
|
||||
|
||||
const tabs = adminRoutes.filter(
|
||||
route =>
|
||||
!group ||
|
||||
route.group === group ||
|
||||
(isOss() && route.group !== 'log')
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPaper>
|
||||
@ -112,29 +53,30 @@ export const AdminTabsMenu: VFC = () => {
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
>
|
||||
{tabs
|
||||
.filter(tab => tab.condition || tab.condition === undefined)
|
||||
.map(tab => (
|
||||
<Tab
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
label={
|
||||
<CenteredNavLink to={tab.link}>
|
||||
{tab.label}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
tab.showEnterpriseBadge
|
||||
)}
|
||||
show={
|
||||
<StyledBadgeContainer>
|
||||
<EnterpriseBadge size={16} />
|
||||
</StyledBadgeContainer>
|
||||
}
|
||||
/>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{tabs.map(tab => (
|
||||
<Tab
|
||||
key={tab.route}
|
||||
value={tab.route?.split('/')?.[2]}
|
||||
label={
|
||||
<CenteredNavLink to={tab.path}>
|
||||
{tab.title}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
tab.menu.mode?.includes('enterprise') &&
|
||||
!tab.menu.mode?.includes('pro') &&
|
||||
isPro() &&
|
||||
showEnterpriseFeaturesInPro
|
||||
)}
|
||||
show={
|
||||
<StyledBadgeContainer>
|
||||
<EnterpriseBadge size={16} />
|
||||
</StyledBadgeContainer>
|
||||
}
|
||||
/>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</StyledPaper>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { CenteredNavLink } from '../menu/CenteredNavLink';
|
||||
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 NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
|
||||
|
||||
const StyledPageContent = styled(PageContent)(() => ({
|
||||
'.page-header': {
|
||||
padding: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Overview',
|
||||
@ -30,8 +24,8 @@ export const Network = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledPageContent
|
||||
headerClass="page-header"
|
||||
<PageContent
|
||||
withTabs
|
||||
header={
|
||||
<Tabs
|
||||
value={pathname}
|
||||
@ -58,7 +52,7 @@ export const Network = () => {
|
||||
<Route path="traffic" element={<NetworkTraffic />} />
|
||||
<Route path="*" element={<NetworkOverview />} />
|
||||
</Routes>
|
||||
</StyledPageContent>
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,189 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { RolesTable } from './RolesTable/RolesTable';
|
||||
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 { 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';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import { READ_ROLE } from '@server/types/permissions';
|
||||
|
||||
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',
|
||||
});
|
||||
import { RolesPage } from './RolesPage';
|
||||
|
||||
export const Roles = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { pathname } = useLocation();
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
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;
|
||||
if (!isEnterprise()) {
|
||||
return <PremiumFeature feature="project-roles" page />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PermissionGuard permissions={[READ_ROLE, ADMIN]}>
|
||||
<StyledPageContent
|
||||
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>
|
||||
<RolesPage />
|
||||
</PermissionGuard>
|
||||
</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 { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||
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 = () => (
|
||||
<div>
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<ServiceAccountsTable />
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
);
|
||||
export const ServiceAccounts = () => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
return (
|
||||
<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 { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||
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 = () => (
|
||||
<div>
|
||||
<InviteLinkBar />
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<UsersList />
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<>
|
||||
<InviteLinkBar />
|
||||
<UsersList />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route path=":id/edit" element={<EditUser />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,14 @@
|
||||
/* eslint react/no-multi-comp:off */
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Icon,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Link,
|
||||
Tab,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from '@mui/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 { ApplicationView } from '../ApplicationView/ApplicationView';
|
||||
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
|
||||
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
@ -27,6 +29,7 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||
|
||||
export const ApplicationEdit = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -37,6 +40,7 @@ export const ApplicationEdit = () => {
|
||||
const { deleteApplication } = useApplicationsApi();
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
@ -91,8 +95,8 @@ export const ApplicationEdit = () => {
|
||||
return <p>Application ({appName}) not found</p>;
|
||||
}
|
||||
return (
|
||||
<PageContent
|
||||
header={
|
||||
<>
|
||||
<PageContent>
|
||||
<PageHeader
|
||||
titleElement={
|
||||
<span
|
||||
@ -133,23 +137,59 @@ export const ApplicationEdit = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="body1">{description || ''}</Typography>
|
||||
<Typography variant="body2">
|
||||
Created: <strong>{formatDate(createdAt)}</strong>
|
||||
</Typography>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_APPLICATION)}
|
||||
show={
|
||||
<div>
|
||||
{renderModal()}
|
||||
<TabNav tabData={tabData} />
|
||||
</div>
|
||||
<Box sx={theme => ({ marginTop: theme.spacing(1) })}>
|
||||
<Typography variant="body1">{description || ''}</Typography>
|
||||
<Typography variant="body2">
|
||||
Created: <strong>{formatDate(createdAt)}</strong>
|
||||
</Typography>
|
||||
</Box>
|
||||
</PageContent>
|
||||
<br />
|
||||
<PageContent
|
||||
withTabs
|
||||
header={
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, tabId) => {
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid container style={{ margin: 0 }}>
|
||||
<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 (
|
||||
<StyledLink key={path} to={link}>
|
||||
<StyledParagraph>
|
||||
|
@ -9,7 +9,19 @@ type EnterpriseBadgeProps = {
|
||||
|
||||
export const EnterpriseBadge: VFC<EnterpriseBadgeProps> = ({ size = 16 }) => (
|
||||
<ThemeMode
|
||||
darkmode={<ProPlanIconLight width={size} height={size} />}
|
||||
lightmode={<ProPlanIcon width={size} height={size} />}
|
||||
darkmode={
|
||||
<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: {
|
||||
padding: theme.spacing(2, 4),
|
||||
},
|
||||
withTabs: {
|
||||
padding: theme.spacing(0, 2),
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: theme.spacing(0, 1),
|
||||
},
|
||||
},
|
||||
bodyContainer: {
|
||||
padding: theme.spacing(4),
|
||||
[theme.breakpoints.down('md')]: {
|
||||
|
@ -20,6 +20,7 @@ interface IPageContentProps extends PaperProps {
|
||||
disableLoading?: boolean;
|
||||
bodyClass?: string;
|
||||
headerClass?: string;
|
||||
withTabs?: boolean;
|
||||
}
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
@ -59,6 +60,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
isLoading = false,
|
||||
disableLoading = false,
|
||||
className,
|
||||
withTabs,
|
||||
...rest
|
||||
}) => {
|
||||
const { classes: styles } = useStyles();
|
||||
@ -69,6 +71,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
{
|
||||
[styles.paddingDisabled]: disablePadding,
|
||||
[styles.borderDisabled]: disableBorder,
|
||||
[styles.withTabs]: withTabs,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -38,8 +38,9 @@ const StyledTypography = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(() => ({
|
||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
@ -87,6 +88,16 @@ const PremiumFeatures = {
|
||||
url: 'https://docs.getunleash.io/reference/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;
|
||||
@ -126,7 +137,7 @@ export const PremiumFeature = ({
|
||||
<>
|
||||
{featureLabel} is a feature available for the{' '}
|
||||
<strong>{plan}</strong>{' '}
|
||||
{plan === FeaturePlan.PRO ? 'plans' : 'plan'}
|
||||
{plan === FeaturePlan.PRO ? 'plans' : 'plan'}.
|
||||
</>
|
||||
);
|
||||
|
||||
@ -148,7 +159,7 @@ export const PremiumFeature = ({
|
||||
<StyledBody tooltip>
|
||||
<StyledTypography>
|
||||
{featureMessage}. You need to upgrade your plan
|
||||
if you want to use it
|
||||
if you want to use it.
|
||||
</StyledTypography>
|
||||
</StyledBody>
|
||||
<StyledButtonContainer>
|
||||
@ -158,7 +169,7 @@ export const PremiumFeature = ({
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Upgrade now
|
||||
Compare plans
|
||||
</StyledLink>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
@ -171,18 +182,26 @@ export const PremiumFeature = ({
|
||||
</StyledTypography>
|
||||
<StyledTypography>
|
||||
You need to upgrade your plan if you want to use
|
||||
it
|
||||
it.
|
||||
</StyledTypography>
|
||||
</StyledBody>
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
href={upgradeUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Upgrade now
|
||||
Compare plans
|
||||
</Button>
|
||||
<Button
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Read about {label}
|
||||
</Button>
|
||||
</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 {
|
||||
value: number;
|
||||
|
@ -5,6 +5,9 @@ import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
|
||||
|
||||
/**
|
||||
* Handle feature flags and configuration for different plans.
|
||||
*/
|
||||
export const filterByConfig =
|
||||
(config: IUiConfig) => (r: INavigationMenuItem) => {
|
||||
if (r.flag) {
|
||||
@ -25,6 +28,12 @@ export const scrollToTop = () => {
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
export const mapRouteLink = (route: INavigationMenuItem) => ({
|
||||
...route,
|
||||
path: route.path.replace('/*', ''),
|
||||
route: route.path,
|
||||
});
|
||||
|
||||
export const trim = (value: string): string => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
|
@ -22,24 +22,18 @@ import { DrawerMenu } from './DrawerMenu/DrawerMenu';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { flexRow, focusable } from 'themes/themeStyles';
|
||||
import { NavigationMenu } from './NavigationMenu/NavigationMenu';
|
||||
import {
|
||||
getRoutes,
|
||||
adminMenuRoutes,
|
||||
getCondensedRoutes,
|
||||
} from 'component/menu/routes';
|
||||
import { getRoutes, getCondensedRoutes } from 'component/menu/routes';
|
||||
import {
|
||||
DarkModeOutlined,
|
||||
KeyboardArrowDown,
|
||||
LightModeOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { filterByConfig } from 'component/common/util';
|
||||
import { filterByConfig, mapRouteLink } from 'component/common/util';
|
||||
import { useId } from 'hooks/useId';
|
||||
import { INavigationMenuItem } from 'interfaces/route';
|
||||
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
|
||||
import { useThemeMode } from 'hooks/useThemeMode';
|
||||
import { Notifications } from 'component/common/Notifications/Notifications';
|
||||
import { filterAdminRoutes } from './filterAdminRoutes';
|
||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
|
||||
|
||||
const StyledHeader = styled(AppBar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@ -109,11 +103,6 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||
borderRadius: 100,
|
||||
}));
|
||||
|
||||
const mapRouteLink = (route: INavigationMenuItem) => ({
|
||||
...route,
|
||||
path: route.path.replace('/*', ''),
|
||||
});
|
||||
|
||||
const Header: VFC = () => {
|
||||
const { onSetThemeMode, themeMode } = useThemeMode();
|
||||
const theme = useTheme();
|
||||
@ -122,20 +111,17 @@ const Header: VFC = () => {
|
||||
const [adminRef, setAdminRef] = useState<HTMLButtonElement | null>(null);
|
||||
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const { uiConfig, isOss, isPro, isEnterprise } = useUiConfig();
|
||||
const { isBilling } = useInstanceStatus();
|
||||
const { uiConfig, isOss } = useUiConfig();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const showApiAccessInConfigure = !uiConfig?.flags?.frontendNavigationUpdate;
|
||||
const showEnterpriseOptionsInPro = Boolean(
|
||||
uiConfig?.flags?.frontendNavigationUpdate
|
||||
);
|
||||
|
||||
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
||||
const onAdminClose = () => setAdminRef(null);
|
||||
const onConfigureClose = () => setConfigRef(null);
|
||||
|
||||
const routes = getRoutes();
|
||||
const adminRoutes = useAdminRoutes();
|
||||
|
||||
const filteredMainRoutes = {
|
||||
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
|
||||
@ -166,20 +152,7 @@ const Header: VFC = () => {
|
||||
)
|
||||
.filter(filterByConfig(uiConfig))
|
||||
.map(mapRouteLink),
|
||||
adminRoutes: adminMenuRoutes
|
||||
.filter(filterByConfig(uiConfig))
|
||||
.filter(route =>
|
||||
filterAdminRoutes(
|
||||
route?.menu,
|
||||
{
|
||||
enterprise: isEnterprise(),
|
||||
pro: isPro(),
|
||||
billing: isBilling,
|
||||
},
|
||||
showEnterpriseOptionsInPro
|
||||
)
|
||||
)
|
||||
.map(mapRouteLink),
|
||||
adminRoutes,
|
||||
};
|
||||
|
||||
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -50,7 +50,7 @@ export const NavigationMenu = ({
|
||||
anchorEl,
|
||||
style,
|
||||
}: INavigationMenuProps) => {
|
||||
const { uiConfig, isPro } = useUiConfig();
|
||||
const { uiConfig, isPro, isOss } = useUiConfig();
|
||||
const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate;
|
||||
|
||||
const showBadge = useCallback(
|
||||
@ -83,29 +83,40 @@ export const NavigationMenu = ({
|
||||
const addDivider =
|
||||
showUpdatedMenu &&
|
||||
previousGroup &&
|
||||
previousGroup !== option.group;
|
||||
previousGroup !== option.group &&
|
||||
(!isOss() || option.group === 'log');
|
||||
|
||||
return [
|
||||
addDivider ? (
|
||||
<Divider variant="middle" key={option.group} />
|
||||
) : null,
|
||||
<MenuItem
|
||||
key={option.path}
|
||||
component={StyledLink}
|
||||
to={option.path}
|
||||
onClick={handleClose}
|
||||
<Tooltip
|
||||
title={
|
||||
showBadge(option?.menu?.mode)
|
||||
? 'This is an Enterprise feature'
|
||||
: ''
|
||||
}
|
||||
arrow
|
||||
placement="left"
|
||||
>
|
||||
<StyledSpan />
|
||||
{option.title}
|
||||
<ConditionallyRender
|
||||
condition={showBadge(option?.menu?.mode)}
|
||||
show={
|
||||
<StyledBadgeContainer>
|
||||
<EnterpriseBadge />
|
||||
</StyledBadgeContainer>
|
||||
}
|
||||
/>
|
||||
</MenuItem>,
|
||||
<MenuItem
|
||||
key={option.path}
|
||||
component={StyledLink}
|
||||
to={option.path}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<StyledSpan />
|
||||
{option.title}
|
||||
<ConditionallyRender
|
||||
condition={showBadge(option?.menu?.mode)}
|
||||
show={
|
||||
<StyledBadgeContainer>
|
||||
<EnterpriseBadge />
|
||||
</StyledBadgeContainer>
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Tooltip>,
|
||||
];
|
||||
})
|
||||
.flat()
|
||||
|
@ -374,14 +374,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
"title": "Archived toggles",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"hidden": false,
|
||||
"menu": {},
|
||||
"path": "/admin",
|
||||
"title": "Admin",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"$$typeof": Symbol(react.lazy),
|
||||
|
@ -4,7 +4,7 @@ import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesLi
|
||||
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
||||
import { AddonList } from 'component/addons/AddonList/AddonList';
|
||||
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 ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
||||
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 { LazyAdmin } from 'component/admin/LazyAdmin';
|
||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
||||
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||
|
||||
@ -385,15 +384,6 @@ export const routes: IRoute[] = [
|
||||
},
|
||||
|
||||
// Admin
|
||||
|
||||
{
|
||||
path: '/admin',
|
||||
title: 'Admin',
|
||||
component: AdminRedirect,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/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) =>
|
||||
routes.find(route => route.path === path);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user