diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx index be65ada320..50be47aa0b 100644 --- a/frontend/src/component/admin/Admin.tsx +++ b/frontend/src/component/admin/Admin.tsx @@ -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 ( <> - } /> + } /> + } /> } /> } /> - } /> - - ) : ( - - ) - } - /> + } /> } /> } /> - } /> - } /> - } - /> - } /> - - ) : ( - - ) - } - /> + } /> + } /> } /> } /> } /> @@ -74,6 +44,7 @@ export const Admin = () => { /> } /> } /> + } /> ); diff --git a/frontend/src/component/admin/AdminIndex.tsx b/frontend/src/component/admin/AdminIndex.tsx new file mode 100644 index 0000000000..8ed728b61d --- /dev/null +++ b/frontend/src/component/admin/AdminIndex.tsx @@ -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 ( + }> + {routeGroups.map(group => ( + ({ marginBottom: theme.spacing(2) })} + > + {group.description} +
    + {group.items.map(route => ( +
  • + + {route.title} + +
  • + ))} +
+
+ ))} +
+ ); +}; diff --git a/frontend/src/component/admin/adminRoutes.ts b/frontend/src/component/admin/adminRoutes.ts new file mode 100644 index 0000000000..ff5e7916ed --- /dev/null +++ b/frontend/src/component/admin/adminRoutes.ts @@ -0,0 +1,111 @@ +import { INavigationMenuItem } from 'interfaces/route'; + +export const adminGroups: Record = { + 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', + }, +]; diff --git a/frontend/src/component/admin/auth/AuthSettings.tsx b/frontend/src/component/admin/auth/AuthSettings.tsx index 64b6e482b3..2f14b9e501 100644 --- a/frontend/src/component/admin/auth/AuthSettings.tsx +++ b/frontend/src/component/admin/auth/AuthSettings.tsx @@ -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 (
- - } - /> + { + setActiveTab(tabId); + }} + indicatorColor="primary" + textColor="primary" + > + {tabs.map((tab, index) => ( + + ))} + + } + /> + } + > - You are running the open-source version of - Unleash. You have to use the Enterprise edition - in order configure Single Sign-on. - - } + show={} /> { } /> + + {tabs.map((tab, index) => ( + + {tab.component} + + ))} +
+ } + /> diff --git a/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx b/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx index 2ecf5a3fe1..d871b6f2ef 100644 --- a/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx +++ b/frontend/src/component/admin/auth/GoogleAuth/GoogleAuth.tsx @@ -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 ( - + <> This integration is deprecated and will be removed in next @@ -240,6 +239,6 @@ export const GoogleAuth = () => { - + ); }; diff --git a/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx b/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx index bb5325f9ee..c5df74206d 100644 --- a/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx +++ b/frontend/src/component/admin/auth/OidcAuth/OidcAuth.tsx @@ -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 ( - + <> @@ -292,6 +291,6 @@ export const OidcAuth = () => { - + ); }; diff --git a/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx b/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx index 9d5ad3db1a..3fa7e54d7e 100644 --- a/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx +++ b/frontend/src/component/admin/auth/PasswordAuth/PasswordAuth.tsx @@ -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 ( - + <>
Overview of administrators on your Unleash instance: @@ -134,6 +133,6 @@ export const PasswordAuth = () => { tokens={tokens} /> -
+ ); }; diff --git a/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx b/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx index 5a041045ba..da4eb30227 100644 --- a/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx +++ b/frontend/src/component/admin/auth/SamlAuth/SamlAuth.tsx @@ -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 ( - + <> @@ -264,6 +263,6 @@ export const SamlAuth = () => { - + ); }; diff --git a/frontend/src/component/menu/Header/filterAdminRoutes.test.ts b/frontend/src/component/admin/filterAdminRoutes.test.ts similarity index 100% rename from frontend/src/component/menu/Header/filterAdminRoutes.test.ts rename to frontend/src/component/admin/filterAdminRoutes.test.ts diff --git a/frontend/src/component/menu/Header/filterAdminRoutes.ts b/frontend/src/component/admin/filterAdminRoutes.ts similarity index 100% rename from frontend/src/component/menu/Header/filterAdminRoutes.ts rename to frontend/src/component/admin/filterAdminRoutes.ts diff --git a/frontend/src/component/admin/groups/GroupsAdmin.tsx b/frontend/src/component/admin/groups/GroupsAdmin.tsx index 9e4db56088..19e909ab23 100644 --- a/frontend/src/component/admin/groups/GroupsAdmin.tsx +++ b/frontend/src/component/admin/groups/GroupsAdmin.tsx @@ -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 = () => ( -
- - - -
-); +export const GroupsAdmin = () => { + const { uiConfig, isEnterprise } = useUiConfig(); + + if (isEnterprise() || uiConfig.flags[UG] === true) { + return ( +
+ + + } /> + } /> + } + /> + } /> + + +
+ ); + } + + return ; +}; diff --git a/frontend/src/component/admin/menu/AdminTabsMenu.tsx b/frontend/src/component/admin/menu/AdminTabsMenu.tsx index ab482bfc0b..685f4f76a2 100644 --- a/frontend/src/component/admin/menu/AdminTabsMenu.tsx +++ b/frontend/src/component/admin/menu/AdminTabsMenu.tsx @@ -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 ( @@ -112,29 +53,30 @@ export const AdminTabsMenu: VFC = () => { scrollButtons="auto" allowScrollButtonsMobile > - {tabs - .filter(tab => tab.condition || tab.condition === undefined) - .map(tab => ( - - {tab.label} - - - - } - /> - - } - /> - ))} + {tabs.map(tab => ( + + {tab.title} + + + + } + /> + + } + /> + ))} ); diff --git a/frontend/src/component/admin/network/Network.tsx b/frontend/src/component/admin/network/Network.tsx index 9a7e610466..a6fa643b24 100644 --- a/frontend/src/component/admin/network/Network.tsx +++ b/frontend/src/component/admin/network/Network.tsx @@ -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 (
- { } /> } /> - +
); }; diff --git a/frontend/src/component/admin/roles/Roles.tsx b/frontend/src/component/admin/roles/Roles.tsx index 680b47a048..cdba9babae 100644 --- a/frontend/src/component/admin/roles/Roles.tsx +++ b/frontend/src/component/admin/roles/Roles.tsx @@ -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(); - - 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 ; + } return (
- - - - - {tabs.map(({ label, path, total }) => ( - - - {label} ({total}) - - - } - /> - ))} - - - - - - - - } - /> - { - setSelectedRole(undefined); - setModalOpen(true); - }} - maxWidth={`${theme.breakpoints.values['sm']}px`} - Icon={Add} - permission={ADMIN} - > - New {type} role - - - - - } - /> - - } - > - - - } - /> - - } - /> - - +
); diff --git a/frontend/src/component/admin/roles/RolesPage.tsx b/frontend/src/component/admin/roles/RolesPage.tsx new file mode 100644 index 0000000000..0f418d1f66 --- /dev/null +++ b/frontend/src/component/admin/roles/RolesPage.tsx @@ -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(); + + 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 ( + + + + + {tabs.map(({ label, path, total }) => ( + + + {label} ({total}) + + + } + /> + ))} + + + + + + + + } + /> + { + setSelectedRole(undefined); + setModalOpen(true); + }} + maxWidth={`${theme.breakpoints.values['sm']}px`} + Icon={Add} + permission={ADMIN} + > + New {type} role + + + + + } + /> + + } + > + + + } + /> + + } + /> + + + ); +}; diff --git a/frontend/src/component/admin/serviceAccounts/ServiceAccounts.tsx b/frontend/src/component/admin/serviceAccounts/ServiceAccounts.tsx index 3e0c9bfb0f..3db836de9d 100644 --- a/frontend/src/component/admin/serviceAccounts/ServiceAccounts.tsx +++ b/frontend/src/component/admin/serviceAccounts/ServiceAccounts.tsx @@ -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 = () => ( -
- - - -
-); +export const ServiceAccounts = () => { + const { isEnterprise } = useUiConfig(); + + return ( +
+ + + + } + elseShow={} + /> +
+ ); +}; diff --git a/frontend/src/component/admin/useAdminRoutes.ts b/frontend/src/component/admin/useAdminRoutes.ts new file mode 100644 index 0000000000..61b85f11de --- /dev/null +++ b/frontend/src/component/admin/useAdminRoutes.ts @@ -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); +}; diff --git a/frontend/src/component/admin/users/UsersAdmin.tsx b/frontend/src/component/admin/users/UsersAdmin.tsx index fd9a7ca8c6..54733ea783 100644 --- a/frontend/src/component/admin/users/UsersAdmin.tsx +++ b/frontend/src/component/admin/users/UsersAdmin.tsx @@ -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 = () => (
- - + + + + + + } + /> + } /> + } /> +
); diff --git a/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx b/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx index 0bbacd33d7..4e9ea4ed8c 100644 --- a/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx +++ b/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx @@ -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

Application ({appName}) not found

; } return ( - + { } /> - } - > -
- {description || ''} - - Created: {formatDate(createdAt)} - -
- - {renderModal()} - - + ({ marginTop: theme.spacing(1) })}> + {description || ''} + + Created: {formatDate(createdAt)} + + +
+
+ { + setActiveTab(tabId); + }} + indicatorColor="primary" + textColor="primary" + > + {tabData.map((tab, index) => ( + + ))} + } - /> - + > + + {renderModal()} + {tabData.map((tab, index) => ( + + {tab.component} + + ))} + + } + /> +
+ ); }; diff --git a/frontend/src/component/application/ApplicationView/ApplicationView.tsx b/frontend/src/component/application/ApplicationView/ApplicationView.tsx index b6c014aa2f..e6e9d0002d 100644 --- a/frontend/src/component/application/ApplicationView/ApplicationView.tsx +++ b/frontend/src/component/application/ApplicationView/ApplicationView.tsx @@ -102,6 +102,7 @@ export const ApplicationView = () => { /> ); + return ( diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx index 01469ee457..dc60d88274 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx @@ -83,14 +83,6 @@ const BreadcrumbNav = () => { } }); - if (index === 0 && path === 'admin') { - return ( - - {path} - - ); - } - return ( diff --git a/frontend/src/component/common/EnterpriseBadge/EnterpriseBadge.tsx b/frontend/src/component/common/EnterpriseBadge/EnterpriseBadge.tsx index 13dd5d0285..1f3528dc06 100644 --- a/frontend/src/component/common/EnterpriseBadge/EnterpriseBadge.tsx +++ b/frontend/src/component/common/EnterpriseBadge/EnterpriseBadge.tsx @@ -9,7 +9,19 @@ type EnterpriseBadgeProps = { export const EnterpriseBadge: VFC = ({ size = 16 }) => ( } - lightmode={} + darkmode={ + + } + lightmode={ + + } /> ); diff --git a/frontend/src/component/common/PageContent/PageContent.styles.ts b/frontend/src/component/common/PageContent/PageContent.styles.ts index 5e905aefa4..35793da879 100644 --- a/frontend/src/component/common/PageContent/PageContent.styles.ts +++ b/frontend/src/component/common/PageContent/PageContent.styles.ts @@ -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')]: { diff --git a/frontend/src/component/common/PageContent/PageContent.tsx b/frontend/src/component/common/PageContent/PageContent.tsx index 83fb5a8e78..0b23f902fe 100644 --- a/frontend/src/component/common/PageContent/PageContent.tsx +++ b/frontend/src/component/common/PageContent/PageContent.tsx @@ -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 = ({ isLoading = false, disableLoading = false, className, + withTabs, ...rest }) => { const { classes: styles } = useStyles(); @@ -69,6 +71,7 @@ export const PageContent: FC = ({ { [styles.paddingDisabled]: disablePadding, [styles.borderDisabled]: disableBorder, + [styles.withTabs]: withTabs, } ); diff --git a/frontend/src/component/common/PremiumFeature/PremiumFeature.tsx b/frontend/src/component/common/PremiumFeature/PremiumFeature.tsx index 4f4d08c31a..7eccb2fc76 100644 --- a/frontend/src/component/common/PremiumFeature/PremiumFeature.tsx +++ b/frontend/src/component/common/PremiumFeature/PremiumFeature.tsx @@ -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{' '} {plan}{' '} - {plan === FeaturePlan.PRO ? 'plans' : 'plan'} + {plan === FeaturePlan.PRO ? 'plans' : 'plan'}. ); @@ -148,7 +159,7 @@ export const PremiumFeature = ({ {featureMessage}. You need to upgrade your plan - if you want to use it + if you want to use it. @@ -158,7 +169,7 @@ export const PremiumFeature = ({ rel="noreferrer" onClick={handleClick} > - Upgrade now + Compare plans @@ -171,18 +182,26 @@ export const PremiumFeature = ({ You need to upgrade your plan if you want to use - it + it. + diff --git a/frontend/src/component/common/TabNav/TabNav/TabNav.tsx b/frontend/src/component/common/TabNav/TabNav/TabNav.tsx deleted file mode 100644 index dbea1fee9c..0000000000 --- a/frontend/src/component/common/TabNav/TabNav/TabNav.tsx +++ /dev/null @@ -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) => ( - - )); - - const renderTabPanels = () => - tabData.map((tab, index) => ( - - {tab.component} - - )); - - return ( - <> - theme.palette.background.paper, - borderBottom: '1px solid', - borderBottomColor: theme => theme.palette.divider, - borderRadius: 0, - }} - > - { - setActiveTab(tabId); - }} - indicatorColor="primary" - textColor="primary" - centered - > - {renderTabs()} - - -
{renderTabPanels()}
- - ); -}; diff --git a/frontend/src/component/common/TabNav/TabPanel/TabPanel.tsx b/frontend/src/component/common/TabNav/TabPanel/TabPanel.tsx index 3c2fc65a39..3478b95901 100644 --- a/frontend/src/component/common/TabNav/TabPanel/TabPanel.tsx +++ b/frontend/src/component/common/TabNav/TabPanel/TabPanel.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; interface ITabPanelProps { value: number; diff --git a/frontend/src/component/common/util.ts b/frontend/src/component/common/util.ts index 97178f6371..b40834b06b 100644 --- a/frontend/src/component/common/util.ts +++ b/frontend/src/component/common/util.ts @@ -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(); diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index 44e6ff48d3..39d10cf0d7 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -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(null); const [configRef, setConfigRef] = useState(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) { diff --git a/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx b/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx index 0fe73ba8cc..da60c200cb 100644 --- a/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx +++ b/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx @@ -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 ? ( ) : null, - - - {option.title} - - - - } - /> - , + + + {option.title} + + + + } + /> + + , ]; }) .flat() diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index 5d2f1a29e6..a02933083e 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -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), diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 7fc5c341a3..484342dcb8 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -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);