1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

Update admin navigation (1-1104-improved-menu-oss) (#4458)

Cleaner tabs navigation for admin tabs


![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 { Billing } from './billing/Billing';
import FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect'; import FlaggedBillingRedirect from './billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
import { CorsAdmin } from './cors'; import { CorsAdmin } from './cors';
import { CreateGroup } from './groups/CreateGroup/CreateGroup';
import { EditGroupContainer } from './groups/EditGroup/EditGroup';
import { Group } from './groups/Group/Group';
import { GroupsAdmin } from './groups/GroupsAdmin'; import { GroupsAdmin } from './groups/GroupsAdmin';
import { InstanceAdmin } from './instance-admin/InstanceAdmin'; import { InstanceAdmin } from './instance-admin/InstanceAdmin';
import { InstancePrivacy } from './instance-privacy/InstancePrivacy'; import { InstancePrivacy } from './instance-privacy/InstancePrivacy';
import { MaintenanceAdmin } from './maintenance'; import { MaintenanceAdmin } from './maintenance';
import { AdminTabsMenu } from './menu/AdminTabsMenu';
import { Network } from './network/Network'; import { Network } from './network/Network';
import { Roles } from './roles/Roles'; import { Roles } from './roles/Roles';
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts'; import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
import CreateUser from './users/CreateUser/CreateUser'; import CreateUser from './users/CreateUser/CreateUser';
import EditUser from './users/EditUser/EditUser';
import { InviteLink } from './users/InviteLink/InviteLink'; import { InviteLink } from './users/InviteLink/InviteLink';
import UsersAdmin from './users/UsersAdmin'; import UsersAdmin from './users/UsersAdmin';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import NotFound from 'component/common/NotFound/NotFound';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import { AdminIndex } from './AdminIndex';
import { AdminTabsMenu } from './menu/AdminTabsMenu';
export const Admin = () => { export const Admin = () => {
const { isEnterprise } = useUiConfig();
return ( return (
<> <>
<AdminTabsMenu /> <AdminTabsMenu />
<Routes> <Routes>
<Route path="users" element={<UsersAdmin />} /> <Route index element={<AdminIndex />} />
<Route path="users/*" element={<UsersAdmin />} />
<Route path="api" element={<ApiTokenPage />} /> <Route path="api" element={<ApiTokenPage />} />
<Route path="api/create-token" element={<CreateApiToken />} /> <Route path="api/create-token" element={<CreateApiToken />} />
<Route path="users/:id/edit" element={<EditUser />} /> <Route path="service-accounts" element={<ServiceAccounts />} />
<Route
path="service-accounts"
element={
isEnterprise() ? (
<ServiceAccounts />
) : (
<PremiumFeature feature="service-accounts" page />
)
}
/>
<Route path="create-user" element={<CreateUser />} /> <Route path="create-user" element={<CreateUser />} />
<Route path="invite-link" element={<InviteLink />} /> <Route path="invite-link" element={<InviteLink />} />
<Route path="groups" element={<GroupsAdmin />} /> <Route path="groups/*" element={<GroupsAdmin />} />
<Route path="groups/create-group" element={<CreateGroup />} /> <Route path="roles/*" element={<Roles />} />
<Route
path="groups/:groupId/edit"
element={<EditGroupContainer />}
/>
<Route path="groups/:groupId" element={<Group />} />
<Route
path="roles/*"
element={
isEnterprise() ? (
<Roles />
) : (
<PremiumFeature feature="project-roles" page />
)
}
/>
<Route path="instance" element={<InstanceAdmin />} /> <Route path="instance" element={<InstanceAdmin />} />
<Route path="network/*" element={<Network />} /> <Route path="network/*" element={<Network />} />
<Route path="maintenance" element={<MaintenanceAdmin />} /> <Route path="maintenance" element={<MaintenanceAdmin />} />
@ -74,6 +44,7 @@ export const Admin = () => {
/> />
<Route path="billing" element={<Billing />} /> <Route path="billing" element={<Billing />} />
<Route path="instance-privacy" element={<InstancePrivacy />} /> <Route path="instance-privacy" element={<InstancePrivacy />} />
<Route path="*" element={<NotFound />} />
</Routes> </Routes>
</> </>
); );

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 { PageContent } from 'component/common/PageContent/PageContent';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -6,9 +6,11 @@ import { OidcAuth } from './OidcAuth/OidcAuth';
import { SamlAuth } from './SamlAuth/SamlAuth'; import { SamlAuth } from './SamlAuth/SamlAuth';
import { PasswordAuth } from './PasswordAuth/PasswordAuth'; import { PasswordAuth } from './PasswordAuth/PasswordAuth';
import { GoogleAuth } from './GoogleAuth/GoogleAuth'; import { GoogleAuth } from './GoogleAuth/GoogleAuth';
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { ADMIN } from '@server/types/permissions'; import { ADMIN } from '@server/types/permissions';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { useState } from 'react';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
export const AuthSettings = () => { export const AuthSettings = () => {
const { authenticationType } = useUiConfig().uiConfig; const { authenticationType } = useUiConfig().uiConfig;
@ -34,24 +36,46 @@ export const AuthSettings = () => {
].filter( ].filter(
item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google' item => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google'
); );
const [activeTab, setActiveTab] = useState(0);
return ( return (
<div> <div>
<PermissionGuard permissions={ADMIN}> <PermissionGuard permissions={ADMIN}>
<PageContent header="Single Sign-On"> <PageContent
<ConditionallyRender withTabs
condition={authenticationType === 'enterprise'} header={
show={<TabNav tabData={tabs} />} <ConditionallyRender
/> condition={authenticationType === 'enterprise'}
show={
<Tabs
value={activeTab}
onChange={(_, tabId) => {
setActiveTab(tabId);
}}
indicatorColor="primary"
textColor="primary"
>
{tabs.map((tab, index) => (
<Tab
key={`${tab.label}_${index}`}
label={tab.label}
id={`tab-${index}`}
aria-controls={`tabpanel-${index}`}
sx={{
minWidth: {
lg: 160,
},
}}
/>
))}
</Tabs>
}
/>
}
>
<ConditionallyRender <ConditionallyRender
condition={authenticationType === 'open-source'} condition={authenticationType === 'open-source'}
show={ show={<PremiumFeature feature="sso" />}
<Alert severity="warning">
You are running the open-source version of
Unleash. You have to use the Enterprise edition
in order configure Single Sign-on.
</Alert>
}
/> />
<ConditionallyRender <ConditionallyRender
condition={authenticationType === 'demo'} condition={authenticationType === 'demo'}
@ -83,6 +107,22 @@ export const AuthSettings = () => {
</Alert> </Alert>
} }
/> />
<ConditionallyRender
condition={authenticationType === 'enterprise'}
show={
<div>
{tabs.map((tab, index) => (
<TabPanel
key={index}
value={activeTab}
index={index}
>
{tab.component}
</TabPanel>
))}
</div>
}
/>
</PageContent> </PageContent>
</PermissionGuard> </PermissionGuard>
</div> </div>

View File

@ -8,7 +8,6 @@ import {
TextField, TextField,
} from '@mui/material'; } from '@mui/material';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings'; import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi'; import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
@ -68,7 +67,7 @@ export const GoogleAuth = () => {
}; };
return ( return (
<PageContent> <>
<Box> <Box>
<Alert severity="error" sx={{ mb: 2 }}> <Alert severity="error" sx={{ mb: 2 }}>
This integration is deprecated and will be removed in next This integration is deprecated and will be removed in next
@ -240,6 +239,6 @@ export const GoogleAuth = () => {
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
</PageContent> </>
); );
}; };

View File

@ -11,7 +11,6 @@ import {
TextField, TextField,
} from '@mui/material'; } from '@mui/material';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm'; import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi'; import useAuthSettingsApi from 'hooks/api/actions/useAuthSettingsApi/useAuthSettingsApi';
@ -82,7 +81,7 @@ export const OidcAuth = () => {
}; };
return ( return (
<PageContent> <>
<Grid container sx={{ mb: 3 }}> <Grid container sx={{ mb: 3 }}>
<Grid item md={12}> <Grid item md={12}>
<Alert severity="info"> <Alert severity="info">
@ -292,6 +291,6 @@ export const OidcAuth = () => {
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
</PageContent> </>
); );
}; };

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, FormControlLabel, Grid, Switch } from '@mui/material'; import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings'; import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
import useAuthSettingsApi, { import useAuthSettingsApi, {
ISimpleAuthSettings, ISimpleAuthSettings,
@ -63,7 +62,7 @@ export const PasswordAuth = () => {
}; };
return ( return (
<PageContent> <>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
Overview of administrators on your Unleash instance: Overview of administrators on your Unleash instance:
@ -134,6 +133,6 @@ export const PasswordAuth = () => {
tokens={tokens} tokens={tokens}
/> />
</form> </form>
</PageContent> </>
); );
}; };

View File

@ -7,7 +7,6 @@ import {
TextField, TextField,
} from '@mui/material'; } from '@mui/material';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm'; import { AutoCreateForm } from '../AutoCreateForm/AutoCreateForm';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -73,7 +72,7 @@ export const SamlAuth = () => {
}; };
return ( return (
<PageContent> <>
<Grid container sx={{ mb: 3 }}> <Grid container sx={{ mb: 3 }}>
<Grid item md={12}> <Grid item md={12}>
<Alert severity="info"> <Alert severity="info">
@ -264,6 +263,6 @@ export const SamlAuth = () => {
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
</PageContent> </>
); );
}; };

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 { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { GroupsList } from './GroupsList/GroupsList'; import { GroupsList } from './GroupsList/GroupsList';
import { ADMIN } from '@server/types/permissions'; import { ADMIN } from '@server/types/permissions';
import { CreateGroup } from './CreateGroup/CreateGroup';
import { EditGroupContainer } from './EditGroup/EditGroup';
import { Group } from './Group/Group';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
export const GroupsAdmin = () => ( export const GroupsAdmin = () => {
<div> const { uiConfig, isEnterprise } = useUiConfig();
<PermissionGuard permissions={ADMIN}>
<GroupsList /> if (isEnterprise() || uiConfig.flags[UG] === true) {
</PermissionGuard> return (
</div> <div>
); <PermissionGuard permissions={ADMIN}>
<Routes>
<Route index element={<GroupsList />} />
<Route path="create-group" element={<CreateGroup />} />
<Route
path=":groupId/edit"
element={<EditGroupContainer />}
/>
<Route path=":groupId" element={<Group />} />
</Routes>
</PermissionGuard>
</div>
);
}
return <PremiumFeature feature="groups" page />;
};

View File

@ -1,11 +1,11 @@
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Paper, styled, Tab, Tabs } from '@mui/material'; import { Paper, styled, Tab, Tabs } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import { CenteredNavLink } from './CenteredNavLink'; import { CenteredNavLink } from './CenteredNavLink';
import { VFC } from 'react'; import { VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge'; import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import { useAdminRoutes } from '../useAdminRoutes';
const StyledPaper = styled(Paper)(({ theme }) => ({ const StyledPaper = styled(Paper)(({ theme }) => ({
marginBottom: '1rem', marginBottom: '1rem',
@ -21,88 +21,29 @@ const StyledBadgeContainer = styled('div')(({ theme }) => ({
})); }));
export const AdminTabsMenu: VFC = () => { export const AdminTabsMenu: VFC = () => {
const { uiConfig, isEnterprise, isPro } = useUiConfig(); const { uiConfig, isPro, isOss } = useUiConfig();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { isBilling } = useInstanceStatus();
const { flags, networkViewEnabled } = uiConfig;
const activeTab = pathname.split('/')[2]; const activeTab = pathname.split('/')[2];
const showEnterpriseFeaturesInPro = const showEnterpriseFeaturesInPro =
uiConfig?.flags?.frontendNavigationUpdate; uiConfig?.flags?.frontendNavigationUpdate;
const tabs = [ const adminRoutes = useAdminRoutes();
{ const group = adminRoutes.find(route =>
value: 'users', pathname.includes(route.path)
label: 'Users', )?.group;
link: '/admin/users',
}, const tabs = adminRoutes.filter(
{ route =>
value: 'service-accounts', !group ||
label: 'Service accounts', route.group === group ||
link: '/admin/service-accounts', (isOss() && route.group !== 'log')
condition: );
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
showEnterpriseBadge: isPro(), if (!group) {
}, return null;
{ }
value: 'groups',
label: 'Groups',
link: '/admin/groups',
condition: flags.UG,
},
{
value: 'roles',
label: 'Roles',
link: '/admin/roles',
condition:
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
showEnterpriseBadge: isPro(),
},
{
value: 'api',
label: 'API access',
link: '/admin/api',
},
{
value: 'cors',
label: 'CORS origins',
link: '/admin/cors',
condition: uiConfig.flags.embedProxyFrontend,
},
{
value: 'auth',
label: 'Single sign-on',
link: '/admin/auth',
},
{
value: 'instance',
label: 'Instance stats',
link: '/admin/instance',
},
{
value: 'network',
label: 'Network',
link: '/admin/network',
condition: networkViewEnabled,
},
{
value: 'maintenance',
label: 'Maintenance',
link: '/admin/maintenance',
},
{
value: 'instance-privacy',
label: 'Instance privacy',
link: '/admin/instance-privacy',
},
{
value: 'billing',
label: 'Billing',
link: '/admin/billing',
condition: isBilling,
},
];
return ( return (
<StyledPaper> <StyledPaper>
@ -112,29 +53,30 @@ export const AdminTabsMenu: VFC = () => {
scrollButtons="auto" scrollButtons="auto"
allowScrollButtonsMobile allowScrollButtonsMobile
> >
{tabs {tabs.map(tab => (
.filter(tab => tab.condition || tab.condition === undefined) <Tab
.map(tab => ( key={tab.route}
<Tab value={tab.route?.split('/')?.[2]}
key={tab.value} label={
value={tab.value} <CenteredNavLink to={tab.path}>
label={ {tab.title}
<CenteredNavLink to={tab.link}> <ConditionallyRender
{tab.label} condition={Boolean(
<ConditionallyRender tab.menu.mode?.includes('enterprise') &&
condition={Boolean( !tab.menu.mode?.includes('pro') &&
tab.showEnterpriseBadge isPro() &&
)} showEnterpriseFeaturesInPro
show={ )}
<StyledBadgeContainer> show={
<EnterpriseBadge size={16} /> <StyledBadgeContainer>
</StyledBadgeContainer> <EnterpriseBadge size={16} />
} </StyledBadgeContainer>
/> }
</CenteredNavLink> />
} </CenteredNavLink>
/> }
))} />
))}
</Tabs> </Tabs>
</StyledPaper> </StyledPaper>
); );

View File

@ -1,6 +1,6 @@
import { lazy } from 'react'; import { lazy } from 'react';
import { styled, Tab, Tabs } from '@mui/material'; import { Tab, Tabs } from '@mui/material';
import { Route, Routes, useLocation } from 'react-router-dom'; import { Route, Routes, useLocation } from 'react-router-dom';
import { CenteredNavLink } from '../menu/CenteredNavLink'; import { CenteredNavLink } from '../menu/CenteredNavLink';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
@ -8,12 +8,6 @@ import { PageContent } from 'component/common/PageContent/PageContent';
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview')); const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic')); const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
const StyledPageContent = styled(PageContent)(() => ({
'.page-header': {
padding: 0,
},
}));
const tabs = [ const tabs = [
{ {
label: 'Overview', label: 'Overview',
@ -30,8 +24,8 @@ export const Network = () => {
return ( return (
<div> <div>
<StyledPageContent <PageContent
headerClass="page-header" withTabs
header={ header={
<Tabs <Tabs
value={pathname} value={pathname}
@ -58,7 +52,7 @@ export const Network = () => {
<Route path="traffic" element={<NetworkTraffic />} /> <Route path="traffic" element={<NetworkTraffic />} />
<Route path="*" element={<NetworkOverview />} /> <Route path="*" element={<NetworkOverview />} />
</Routes> </Routes>
</StyledPageContent> </PageContent>
</div> </div>
); );
}; };

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 { ADMIN } from 'component/providers/AccessProvider/permissions';
import { RolesTable } from './RolesTable/RolesTable';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { PageContent } from 'component/common/PageContent/PageContent';
import { Tab, Tabs, styled, useMediaQuery } from '@mui/material';
import { Route, Routes, useLocation } from 'react-router-dom';
import { CenteredNavLink } from '../menu/CenteredNavLink';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PROJECT_ROLE_TYPE, ROOT_ROLE_TYPE } from '@server/util/constants'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import { Search } from 'component/common/Search/Search';
import theme from 'themes/theme';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Add } from '@mui/icons-material';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { IRole } from 'interfaces/role';
import { READ_ROLE } from '@server/types/permissions'; import { READ_ROLE } from '@server/types/permissions';
import { RolesPage } from './RolesPage';
const StyledPageContent = styled(PageContent)(({ theme }) => ({
'& .page-header': {
padding: theme.spacing(0, 4),
[theme.breakpoints.down('md')]: {
padding: theme.spacing(1),
},
},
}));
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}));
const StyledTabsContainer = styled('div')({
flex: 1,
});
const StyledActions = styled('div')({
display: 'flex',
alignItems: 'center',
});
export const Roles = () => { export const Roles = () => {
const { uiConfig } = useUiConfig(); const { isEnterprise } = useUiConfig();
const { pathname } = useLocation();
const { roles, projectRoles, loading } = useRoles(); if (!isEnterprise()) {
return <PremiumFeature feature="project-roles" page />;
const [searchValue, setSearchValue] = useState(''); }
const [modalOpen, setModalOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<IRole>();
const tabs = uiConfig.flags.customRootRoles
? [
{
label: 'Root roles',
path: '/admin/roles',
total: roles.length,
},
{
label: 'Project roles',
path: '/admin/roles/project-roles',
total: projectRoles.length,
},
]
: [
{
label: 'Project roles',
path: '/admin/roles',
total: projectRoles.length,
},
];
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const type =
!uiConfig.flags.customRootRoles || pathname.includes('project-roles')
? PROJECT_ROLE_TYPE
: ROOT_ROLE_TYPE;
return ( return (
<div> <div>
<PermissionGuard permissions={[READ_ROLE, ADMIN]}> <PermissionGuard permissions={[READ_ROLE, ADMIN]}>
<StyledPageContent <RolesPage />
headerClass="page-header"
bodyClass="page-body"
isLoading={loading}
header={
<>
<StyledHeader>
<StyledTabsContainer>
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
allowScrollButtonsMobile
>
{tabs.map(({ label, path, total }) => (
<Tab
key={label}
value={path}
label={
<CenteredNavLink to={path}>
<span>
{label} ({total})
</span>
</CenteredNavLink>
}
/>
))}
</Tabs>
</StyledTabsContainer>
<StyledActions>
<ConditionallyRender
condition={!isSmallScreen}
show={
<>
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
<PageHeader.Divider />
</>
}
/>
<ResponsiveButton
onClick={() => {
setSelectedRole(undefined);
setModalOpen(true);
}}
maxWidth={`${theme.breakpoints.values['sm']}px`}
Icon={Add}
permission={ADMIN}
>
New {type} role
</ResponsiveButton>
</StyledActions>
</StyledHeader>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
}
/>
</>
}
>
<Routes>
<Route
path="project-roles"
element={
<RolesTable
type={PROJECT_ROLE_TYPE}
searchValue={searchValue}
modalOpen={modalOpen}
setModalOpen={setModalOpen}
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
}
/>
<Route
path="*"
element={
<RolesTable
type={
uiConfig.flags.customRootRoles
? ROOT_ROLE_TYPE
: PROJECT_ROLE_TYPE
}
searchValue={searchValue}
modalOpen={modalOpen}
setModalOpen={setModalOpen}
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
}
/>
</Routes>
</StyledPageContent>
</PermissionGuard> </PermissionGuard>
</div> </div>
); );

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 { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { ServiceAccountsTable } from './ServiceAccountsTable/ServiceAccountsTable'; import { ServiceAccountsTable } from './ServiceAccountsTable/ServiceAccountsTable';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { AdminTabsMenu } from '../menu/AdminTabsMenu';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
export const ServiceAccounts = () => ( export const ServiceAccounts = () => {
<div> const { isEnterprise } = useUiConfig();
<PermissionGuard permissions={ADMIN}>
<ServiceAccountsTable /> return (
</PermissionGuard> <div>
</div> <ConditionallyRender
); condition={isEnterprise()}
show={
<PermissionGuard permissions={ADMIN}>
<ServiceAccountsTable />
</PermissionGuard>
}
elseShow={<PremiumFeature feature="service-accounts" page />}
/>
</div>
);
};

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 { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { InviteLinkBar } from './InviteLinkBar/InviteLinkBar'; import { InviteLinkBar } from './InviteLinkBar/InviteLinkBar';
import { Route, Routes } from 'react-router-dom';
import EditUser from './EditUser/EditUser';
import NotFound from 'component/common/NotFound/NotFound';
export const UsersAdmin = () => ( export const UsersAdmin = () => (
<div> <div>
<InviteLinkBar />
<PermissionGuard permissions={ADMIN}> <PermissionGuard permissions={ADMIN}>
<UsersList /> <Routes>
<Route
index
element={
<>
<InviteLinkBar />
<UsersList />
</>
}
/>
<Route path=":id/edit" element={<EditUser />} />
<Route path="*" element={<NotFound />} />
</Routes>
</PermissionGuard> </PermissionGuard>
</div> </div>
); );

View File

@ -1,11 +1,14 @@
/* eslint react/no-multi-comp:off */ /* eslint react/no-multi-comp:off */
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { import {
Box,
Avatar, Avatar,
Icon, Icon,
IconButton, IconButton,
LinearProgress, LinearProgress,
Link, Link,
Tab,
Tabs,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Link as LinkIcon } from '@mui/icons-material'; import { Link as LinkIcon } from '@mui/icons-material';
@ -13,7 +16,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions'; import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions';
import { ApplicationView } from '../ApplicationView/ApplicationView'; import { ApplicationView } from '../ApplicationView/ApplicationView';
import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate'; import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate';
import { TabNav } from 'component/common/TabNav/TabNav/TabNav';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
@ -27,6 +29,7 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton
import { formatDateYMD } from 'utils/formatDate'; import { formatDateYMD } from 'utils/formatDate';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
export const ApplicationEdit = () => { export const ApplicationEdit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -37,6 +40,7 @@ export const ApplicationEdit = () => {
const { deleteApplication } = useApplicationsApi(); const { deleteApplication } = useApplicationsApi();
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const [activeTab, setActiveTab] = useState(0);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
@ -91,8 +95,8 @@ export const ApplicationEdit = () => {
return <p>Application ({appName}) not found</p>; return <p>Application ({appName}) not found</p>;
} }
return ( return (
<PageContent <>
header={ <PageContent>
<PageHeader <PageHeader
titleElement={ titleElement={
<span <span
@ -133,23 +137,59 @@ export const ApplicationEdit = () => {
</> </>
} }
/> />
} <Box sx={theme => ({ marginTop: theme.spacing(1) })}>
> <Typography variant="body1">{description || ''}</Typography>
<div> <Typography variant="body2">
<Typography variant="body1">{description || ''}</Typography> Created: <strong>{formatDate(createdAt)}</strong>
<Typography variant="body2"> </Typography>
Created: <strong>{formatDate(createdAt)}</strong> </Box>
</Typography> </PageContent>
</div> <br />
<ConditionallyRender <PageContent
condition={hasAccess(UPDATE_APPLICATION)} withTabs
show={ header={
<div> <Tabs
{renderModal()} value={activeTab}
<TabNav tabData={tabData} /> onChange={(_, tabId) => {
</div> setActiveTab(tabId);
}}
indicatorColor="primary"
textColor="primary"
>
{tabData.map((tab, index) => (
<Tab
key={`${tab.label}_${index}`}
label={tab.label}
id={`tab-${index}`}
aria-controls={`tabpanel-${index}`}
sx={{
minWidth: {
lg: 160,
},
}}
/>
))}
</Tabs>
} }
/> >
</PageContent> <ConditionallyRender
condition={hasAccess(UPDATE_APPLICATION)}
show={
<div>
{renderModal()}
{tabData.map((tab, index) => (
<TabPanel
key={index}
value={activeTab}
index={index}
>
{tab.component}
</TabPanel>
))}
</div>
}
/>
</PageContent>
</>
); );
}; };

View File

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

View File

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

View File

@ -9,7 +9,19 @@ type EnterpriseBadgeProps = {
export const EnterpriseBadge: VFC<EnterpriseBadgeProps> = ({ size = 16 }) => ( export const EnterpriseBadge: VFC<EnterpriseBadgeProps> = ({ size = 16 }) => (
<ThemeMode <ThemeMode
darkmode={<ProPlanIconLight width={size} height={size} />} darkmode={
lightmode={<ProPlanIcon width={size} height={size} />} <ProPlanIconLight
width={size}
height={size}
style={{ filter: 'grayscale(100%)', opacity: 0.51 }}
/>
}
lightmode={
<ProPlanIcon
width={size}
height={size}
style={{ filter: 'grayscale(100%)', opacity: 0.6 }}
/>
}
/> />
); );

View File

@ -4,6 +4,12 @@ export const useStyles = makeStyles()(theme => ({
headerPadding: { headerPadding: {
padding: theme.spacing(2, 4), padding: theme.spacing(2, 4),
}, },
withTabs: {
padding: theme.spacing(0, 2),
[theme.breakpoints.down('md')]: {
padding: theme.spacing(0, 1),
},
},
bodyContainer: { bodyContainer: {
padding: theme.spacing(4), padding: theme.spacing(4),
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {

View File

@ -20,6 +20,7 @@ interface IPageContentProps extends PaperProps {
disableLoading?: boolean; disableLoading?: boolean;
bodyClass?: string; bodyClass?: string;
headerClass?: string; headerClass?: string;
withTabs?: boolean;
} }
const StyledHeader = styled('div')(({ theme }) => ({ const StyledHeader = styled('div')(({ theme }) => ({
@ -59,6 +60,7 @@ export const PageContent: FC<IPageContentProps> = ({
isLoading = false, isLoading = false,
disableLoading = false, disableLoading = false,
className, className,
withTabs,
...rest ...rest
}) => { }) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -69,6 +71,7 @@ export const PageContent: FC<IPageContentProps> = ({
{ {
[styles.paddingDisabled]: disablePadding, [styles.paddingDisabled]: disablePadding,
[styles.borderDisabled]: disableBorder, [styles.borderDisabled]: disableBorder,
[styles.withTabs]: withTabs,
} }
); );

View File

@ -38,8 +38,9 @@ const StyledTypography = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
const StyledButtonContainer = styled('div')(() => ({ const StyledButtonContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
gap: theme.spacing(1.5),
})); }));
const StyledLink = styled(Link)(({ theme }) => ({ const StyledLink = styled(Link)(({ theme }) => ({
@ -87,6 +88,16 @@ const PremiumFeatures = {
url: 'https://docs.getunleash.io/reference/login-history', url: 'https://docs.getunleash.io/reference/login-history',
label: 'Login history', label: 'Login history',
}, },
groups: {
plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/rbac#user-groups',
label: 'User groups',
},
sso: {
plan: FeaturePlan.PRO,
url: 'https://docs.getunleash.io/reference/rbac#user-group-sso-integration',
label: 'Single Sign-On',
},
}; };
type PremiumFeatureType = keyof typeof PremiumFeatures; type PremiumFeatureType = keyof typeof PremiumFeatures;
@ -126,7 +137,7 @@ export const PremiumFeature = ({
<> <>
{featureLabel} is a feature available for the{' '} {featureLabel} is a feature available for the{' '}
<strong>{plan}</strong>{' '} <strong>{plan}</strong>{' '}
{plan === FeaturePlan.PRO ? 'plans' : 'plan'} {plan === FeaturePlan.PRO ? 'plans' : 'plan'}.
</> </>
); );
@ -148,7 +159,7 @@ export const PremiumFeature = ({
<StyledBody tooltip> <StyledBody tooltip>
<StyledTypography> <StyledTypography>
{featureMessage}. You need to upgrade your plan {featureMessage}. You need to upgrade your plan
if you want to use it if you want to use it.
</StyledTypography> </StyledTypography>
</StyledBody> </StyledBody>
<StyledButtonContainer> <StyledButtonContainer>
@ -158,7 +169,7 @@ export const PremiumFeature = ({
rel="noreferrer" rel="noreferrer"
onClick={handleClick} onClick={handleClick}
> >
Upgrade now Compare plans
</StyledLink> </StyledLink>
</StyledButtonContainer> </StyledButtonContainer>
</> </>
@ -171,18 +182,26 @@ export const PremiumFeature = ({
</StyledTypography> </StyledTypography>
<StyledTypography> <StyledTypography>
You need to upgrade your plan if you want to use You need to upgrade your plan if you want to use
it it.
</StyledTypography> </StyledTypography>
</StyledBody> </StyledBody>
<StyledButtonContainer> <StyledButtonContainer>
<Button <Button
variant="outlined" variant="contained"
href={upgradeUrl} href={upgradeUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={handleClick} onClick={handleClick}
> >
Upgrade now Compare plans
</Button>
<Button
href={url}
target="_blank"
rel="noreferrer"
onClick={handleClick}
>
Read about {label}
</Button> </Button>
</StyledButtonContainer> </StyledButtonContainer>
</> </>

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 { interface ITabPanelProps {
value: number; value: number;

View File

@ -5,6 +5,9 @@ import { IFeatureVariant } from 'interfaces/featureToggle';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal'; import { IFeatureVariantEdit } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/EnvironmentVariantsModal';
/**
* Handle feature flags and configuration for different plans.
*/
export const filterByConfig = export const filterByConfig =
(config: IUiConfig) => (r: INavigationMenuItem) => { (config: IUiConfig) => (r: INavigationMenuItem) => {
if (r.flag) { if (r.flag) {
@ -25,6 +28,12 @@ export const scrollToTop = () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}; };
export const mapRouteLink = (route: INavigationMenuItem) => ({
...route,
path: route.path.replace('/*', ''),
route: route.path,
});
export const trim = (value: string): string => { export const trim = (value: string): string => {
if (value && value.trim) { if (value && value.trim) {
return value.trim(); return value.trim();

View File

@ -22,24 +22,18 @@ import { DrawerMenu } from './DrawerMenu/DrawerMenu';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { flexRow, focusable } from 'themes/themeStyles'; import { flexRow, focusable } from 'themes/themeStyles';
import { NavigationMenu } from './NavigationMenu/NavigationMenu'; import { NavigationMenu } from './NavigationMenu/NavigationMenu';
import { import { getRoutes, getCondensedRoutes } from 'component/menu/routes';
getRoutes,
adminMenuRoutes,
getCondensedRoutes,
} from 'component/menu/routes';
import { import {
DarkModeOutlined, DarkModeOutlined,
KeyboardArrowDown, KeyboardArrowDown,
LightModeOutlined, LightModeOutlined,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { filterByConfig } from 'component/common/util'; import { filterByConfig, mapRouteLink } from 'component/common/util';
import { useId } from 'hooks/useId'; import { useId } from 'hooks/useId';
import { INavigationMenuItem } from 'interfaces/route';
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
import { useThemeMode } from 'hooks/useThemeMode'; import { useThemeMode } from 'hooks/useThemeMode';
import { Notifications } from 'component/common/Notifications/Notifications'; import { Notifications } from 'component/common/Notifications/Notifications';
import { filterAdminRoutes } from './filterAdminRoutes'; import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
const StyledHeader = styled(AppBar)(({ theme }) => ({ const StyledHeader = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -109,11 +103,6 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({
borderRadius: 100, borderRadius: 100,
})); }));
const mapRouteLink = (route: INavigationMenuItem) => ({
...route,
path: route.path.replace('/*', ''),
});
const Header: VFC = () => { const Header: VFC = () => {
const { onSetThemeMode, themeMode } = useThemeMode(); const { onSetThemeMode, themeMode } = useThemeMode();
const theme = useTheme(); const theme = useTheme();
@ -122,20 +111,17 @@ const Header: VFC = () => {
const [adminRef, setAdminRef] = useState<HTMLButtonElement | null>(null); const [adminRef, setAdminRef] = useState<HTMLButtonElement | null>(null);
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null); const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
const { uiConfig, isOss, isPro, isEnterprise } = useUiConfig(); const { uiConfig, isOss } = useUiConfig();
const { isBilling } = useInstanceStatus();
const smallScreen = useMediaQuery(theme.breakpoints.down('md')); const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const showApiAccessInConfigure = !uiConfig?.flags?.frontendNavigationUpdate; const showApiAccessInConfigure = !uiConfig?.flags?.frontendNavigationUpdate;
const showEnterpriseOptionsInPro = Boolean(
uiConfig?.flags?.frontendNavigationUpdate
);
const toggleDrawer = () => setOpenDrawer(prev => !prev); const toggleDrawer = () => setOpenDrawer(prev => !prev);
const onAdminClose = () => setAdminRef(null); const onAdminClose = () => setAdminRef(null);
const onConfigureClose = () => setConfigRef(null); const onConfigureClose = () => setConfigRef(null);
const routes = getRoutes(); const routes = getRoutes();
const adminRoutes = useAdminRoutes();
const filteredMainRoutes = { const filteredMainRoutes = {
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes) mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
@ -166,20 +152,7 @@ const Header: VFC = () => {
) )
.filter(filterByConfig(uiConfig)) .filter(filterByConfig(uiConfig))
.map(mapRouteLink), .map(mapRouteLink),
adminRoutes: adminMenuRoutes adminRoutes,
.filter(filterByConfig(uiConfig))
.filter(route =>
filterAdminRoutes(
route?.menu,
{
enterprise: isEnterprise(),
pro: isPro(),
billing: isBilling,
},
showEnterpriseOptionsInPro
)
)
.map(mapRouteLink),
}; };
if (smallScreen) { if (smallScreen) {

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 { Menu, MenuItem, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -50,7 +50,7 @@ export const NavigationMenu = ({
anchorEl, anchorEl,
style, style,
}: INavigationMenuProps) => { }: INavigationMenuProps) => {
const { uiConfig, isPro } = useUiConfig(); const { uiConfig, isPro, isOss } = useUiConfig();
const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate; const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate;
const showBadge = useCallback( const showBadge = useCallback(
@ -83,29 +83,40 @@ export const NavigationMenu = ({
const addDivider = const addDivider =
showUpdatedMenu && showUpdatedMenu &&
previousGroup && previousGroup &&
previousGroup !== option.group; previousGroup !== option.group &&
(!isOss() || option.group === 'log');
return [ return [
addDivider ? ( addDivider ? (
<Divider variant="middle" key={option.group} /> <Divider variant="middle" key={option.group} />
) : null, ) : null,
<MenuItem <Tooltip
key={option.path} title={
component={StyledLink} showBadge(option?.menu?.mode)
to={option.path} ? 'This is an Enterprise feature'
onClick={handleClose} : ''
}
arrow
placement="left"
> >
<StyledSpan /> <MenuItem
{option.title} key={option.path}
<ConditionallyRender component={StyledLink}
condition={showBadge(option?.menu?.mode)} to={option.path}
show={ onClick={handleClose}
<StyledBadgeContainer> >
<EnterpriseBadge /> <StyledSpan />
</StyledBadgeContainer> {option.title}
} <ConditionallyRender
/> condition={showBadge(option?.menu?.mode)}
</MenuItem>, show={
<StyledBadgeContainer>
<EnterpriseBadge />
</StyledBadgeContainer>
}
/>
</MenuItem>
</Tooltip>,
]; ];
}) })
.flat() .flat()

View File

@ -374,14 +374,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "Archived toggles", "title": "Archived toggles",
"type": "protected", "type": "protected",
}, },
{
"component": [Function],
"hidden": false,
"menu": {},
"path": "/admin",
"title": "Admin",
"type": "protected",
},
{ {
"component": { "component": {
"$$typeof": Symbol(react.lazy), "$$typeof": Symbol(react.lazy),

View File

@ -4,7 +4,7 @@ import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesLi
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList'; import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
import { AddonList } from 'component/addons/AddonList/AddonList'; import { AddonList } from 'component/addons/AddonList/AddonList';
import Login from 'component/user/Login/Login'; import Login from 'component/user/Login/Login';
import { EEA, P, SE, UG } from 'component/common/flags'; import { EEA, P, SE } from 'component/common/flags';
import { NewUser } from 'component/user/NewUser/NewUser'; import { NewUser } from 'component/user/NewUser/NewUser';
import ResetPassword from 'component/user/ResetPassword/ResetPassword'; import ResetPassword from 'component/user/ResetPassword/ResetPassword';
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword'; import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
@ -42,7 +42,6 @@ import { LazyCreateProject } from 'component/project/Project/CreateProject/LazyC
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView'; import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
import { LazyAdmin } from 'component/admin/LazyAdmin'; import { LazyAdmin } from 'component/admin/LazyAdmin';
import { LazyProject } from 'component/project/Project/LazyProject'; import { LazyProject } from 'component/project/Project/LazyProject';
import { AdminRedirect } from 'component/admin/AdminRedirect';
import { LoginHistory } from 'component/loginHistory/LoginHistory'; import { LoginHistory } from 'component/loginHistory/LoginHistory';
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList'; import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
@ -385,15 +384,6 @@ export const routes: IRoute[] = [
}, },
// Admin // Admin
{
path: '/admin',
title: 'Admin',
component: AdminRedirect,
hidden: false,
type: 'protected',
menu: {},
},
{ {
path: '/admin/*', path: '/admin/*',
title: 'Admin', title: 'Admin',
@ -452,109 +442,6 @@ export const routes: IRoute[] = [
}, },
]; ];
export const adminMenuRoutes: INavigationMenuItem[] = [
{
path: '/admin/users',
title: 'Users',
menu: { adminSettings: true },
group: 'users',
},
{
path: '/admin/service-accounts',
title: 'Service accounts',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/groups',
title: 'Groups',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
flag: UG,
group: 'users',
},
{
path: '/admin/roles/*',
title: 'Roles',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'users',
},
{
path: '/admin/api',
title: 'API access',
flag: 'frontendNavigationUpdate',
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/cors',
title: 'CORS origins',
flag: 'embedProxyFrontend',
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/auth',
title: 'Single sign-on',
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/network/*',
title: 'Network',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
configFlag: 'networkViewEnabled',
group: 'instance',
},
{
path: '/admin/maintenance',
title: 'Maintenance',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/instance',
title: 'Instance stats',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/instance-privacy',
title: 'Instance privacy',
menu: { adminSettings: true },
group: 'instance',
},
{
path: '/admin/admin-invoices',
title: 'Billing & invoices',
menu: { adminSettings: true, mode: ['pro'], billing: true },
group: 'instance',
},
{
path: '/admin/logins',
title: 'Login history',
menu: {
adminSettings: true,
mode: ['enterprise'],
},
group: 'log',
},
{
path: '/history',
title: 'Event log',
menu: { adminSettings: true },
group: 'log',
},
];
export const getRoute = (path: string) => export const getRoute = (path: string) =>
routes.find(route => route.path === path); routes.find(route => route.path === path);