1
0
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:
Tymoteusz Czech 2023-08-10 09:28:10 +02:00 committed by GitHub
parent 95f4f641b5
commit 8ee031e978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 714 additions and 649 deletions

View File

@ -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>
</>
);

View 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>
);
};

View 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',
},
];

View File

@ -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>

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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 />;
};

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>
);

View 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>
);
};

View File

@ -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>
);
};

View 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);
};

View File

@ -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>
);

View File

@ -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>
</>
);
};

View File

@ -102,6 +102,7 @@ export const ApplicationView = () => {
/>
</ListItem>
);
return (
<Grid container style={{ margin: 0 }}>
<Grid item xl={6} md={6} xs={12}>

View File

@ -83,14 +83,6 @@ const BreadcrumbNav = () => {
}
});
if (index === 0 && path === 'admin') {
return (
<StyledParagraph key={path}>
{path}
</StyledParagraph>
);
}
return (
<StyledLink key={path} to={link}>
<StyledParagraph>

View File

@ -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 }}
/>
}
/>
);

View File

@ -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')]: {

View File

@ -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,
}
);

View File

@ -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>
</>

View File

@ -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>
</>
);
};

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import { ReactNode } from 'react';
interface ITabPanelProps {
value: number;

View File

@ -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();

View File

@ -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) {

View File

@ -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()

View File

@ -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),

View File

@ -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);