1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Menu update - improvements and mobile version (#4423)

## About the changes


![image](https://github.com/Unleash/unleash/assets/2625371/b12718dd-3bfa-472d-9cc0-3b4480e77497)
This commit is contained in:
Tymoteusz Czech 2023-08-07 18:13:56 +02:00 committed by GitHub
parent 555b27a653
commit 6ae1887aa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 384 additions and 176 deletions

View File

@ -1 +1,6 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.739.811a1 1 0 0 0-1.479 0L9.947 2.253a1 1 0 0 1-1.043.28L7.045 1.94a1 1 0 0 0-1.28.739l-.417 1.906a1 1 0 0 1-.763.763l-1.906.417a1 1 0 0 0-.739 1.28l.592 1.859a1 1 0 0 1-.28 1.042L.812 11.261a1 1 0 0 0 0 1.478l1.442 1.314a1 1 0 0 1 .28 1.043l-.593 1.858a1 1 0 0 0 .74 1.28l1.905.417a1 1 0 0 1 .763.764l.417 1.905a1 1 0 0 0 1.28.74l1.859-.592a1 1 0 0 1 1.043.279l1.313 1.442a1 1 0 0 0 1.479 0l1.313-1.442a1 1 0 0 1 1.043-.28l1.859.592a1 1 0 0 0 1.28-.739l.417-1.905a1 1 0 0 1 .763-.764l1.906-.416a1 1 0 0 0 .739-1.28l-.592-1.86a1 1 0 0 1 .28-1.042l1.441-1.314a1 1 0 0 0 0-1.478l-1.442-1.314a1 1 0 0 1-.279-1.042l.592-1.86a1 1 0 0 0-.74-1.28l-1.905-.416a1 1 0 0 1-.763-.763l-.417-1.906a1 1 0 0 0-1.28-.74l-1.859.593a1 1 0 0 1-1.043-.28L12.74.811ZM7 7h3.434v6.565h3.13V7h3.436v10H7V7Z" fill="#E2E1F5"/><path d="M13.565 13.565H17V17h-3.435v-3.435Z" fill="#817AFE"/></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12.739.811a1 1 0 0 0-1.479 0L9.947 2.253a1 1 0 0 1-1.043.28L7.045 1.94a1 1 0 0 0-1.28.739l-.417 1.906a1 1 0 0 1-.763.763l-1.906.417a1 1 0 0 0-.739 1.28l.592 1.859a1 1 0 0 1-.28 1.042L.812 11.261a1 1 0 0 0 0 1.478l1.442 1.314a1 1 0 0 1 .28 1.043l-.593 1.858a1 1 0 0 0 .74 1.28l1.905.417a1 1 0 0 1 .763.764l.417 1.905a1 1 0 0 0 1.28.74l1.859-.592a1 1 0 0 1 1.043.279l1.313 1.442a1 1 0 0 0 1.479 0l1.313-1.442a1 1 0 0 1 1.043-.28l1.859.592a1 1 0 0 0 1.28-.739l.417-1.905a1 1 0 0 1 .763-.764l1.906-.416a1 1 0 0 0 .739-1.28l-.592-1.86a1 1 0 0 1 .28-1.042l1.441-1.314a1 1 0 0 0 0-1.478l-1.442-1.314a1 1 0 0 1-.279-1.042l.592-1.86a1 1 0 0 0-.74-1.28l-1.905-.416a1 1 0 0 1-.763-.763l-.417-1.906a1 1 0 0 0-1.28-.74l-1.859.593a1 1 0 0 1-1.043-.28L12.74.811ZM7 7h3.434v6.565h3.13V7h3.436v10H7V7Z"
fill="#E2E1F5" />
<path d="M13.565 13.565H17V17h-3.435v-3.435Z" fill="#817AFE" />
</svg>

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.2608 0.811331C11.6574 0.375976 12.3426 0.375977 12.7392 0.811332L14.0529 2.25322C14.316 2.54202 14.7233 2.65114 15.0955 2.53259L16.9542 1.94073C17.5154 1.76203 18.1087 2.10458 18.2345 2.67993L18.6513 4.58549C18.7347 4.96716 19.0328 5.26527 19.4145 5.34875L21.3201 5.7655C21.8954 5.89133 22.238 6.48465 22.0593 7.04583L21.4674 8.90447C21.3489 9.27675 21.458 9.68397 21.7468 9.9471L23.1887 11.2608C23.624 11.6574 23.624 12.3426 23.1887 12.7392L21.7468 14.0529C21.458 14.316 21.3489 14.7233 21.4674 15.0955L22.0593 16.9542C22.238 17.5154 21.8954 18.1087 21.3201 18.2345L19.4145 18.6513C19.0328 18.7347 18.7347 19.0328 18.6513 19.4145L18.2345 21.3201C18.1087 21.8954 17.5154 22.238 16.9542 22.0593L15.0955 21.4674C14.7233 21.3489 14.316 21.458 14.0529 21.7468L12.7392 23.1887C12.3426 23.624 11.6574 23.624 11.2608 23.1887L9.9471 21.7468C9.68397 21.458 9.27675 21.3489 8.90447 21.4674L7.04583 22.0593C6.48465 22.238 5.89133 21.8954 5.7655 21.3201L5.34875 19.4145C5.26527 19.0328 4.96716 18.7347 4.58549 18.6513L2.67993 18.2345C2.10458 18.1087 1.76203 17.5154 1.94073 16.9542L2.53259 15.0955C2.65114 14.7233 2.54202 14.316 2.25322 14.0529L0.811331 12.7392C0.375976 12.3426 0.375977 11.6574 0.811332 11.2608L2.25322 9.9471C2.54202 9.68397 2.65114 9.27675 2.53259 8.90447L1.94073 7.04583C1.76203 6.48465 2.10458 5.89133 2.67993 5.7655L4.58549 5.34875C4.96716 5.26527 5.26527 4.96716 5.34875 4.58549L5.7655 2.67993C5.89133 2.10458 6.48465 1.76203 7.04583 1.94073L8.90447 2.53259C9.27675 2.65114 9.68397 2.54202 9.9471 2.25322L11.2608 0.811331Z" fill="#1A4049"/>
<path d="M10.4351 7.3042H7.30469V16.6955H16.696V7.3042H13.5656V13.5651H10.4351V7.3042Z" fill="white"/>
<path d="M13.5664 13.5649H16.6968V16.6954H13.5664V13.5649Z" fill="#817AFE"/>
<path
d="M11.2608 0.811331C11.6574 0.375976 12.3426 0.375977 12.7392 0.811332L14.0529 2.25322C14.316 2.54202 14.7233 2.65114 15.0955 2.53259L16.9542 1.94073C17.5154 1.76203 18.1087 2.10458 18.2345 2.67993L18.6513 4.58549C18.7347 4.96716 19.0328 5.26527 19.4145 5.34875L21.3201 5.7655C21.8954 5.89133 22.238 6.48465 22.0593 7.04583L21.4674 8.90447C21.3489 9.27675 21.458 9.68397 21.7468 9.9471L23.1887 11.2608C23.624 11.6574 23.624 12.3426 23.1887 12.7392L21.7468 14.0529C21.458 14.316 21.3489 14.7233 21.4674 15.0955L22.0593 16.9542C22.238 17.5154 21.8954 18.1087 21.3201 18.2345L19.4145 18.6513C19.0328 18.7347 18.7347 19.0328 18.6513 19.4145L18.2345 21.3201C18.1087 21.8954 17.5154 22.238 16.9542 22.0593L15.0955 21.4674C14.7233 21.3489 14.316 21.458 14.0529 21.7468L12.7392 23.1887C12.3426 23.624 11.6574 23.624 11.2608 23.1887L9.9471 21.7468C9.68397 21.458 9.27675 21.3489 8.90447 21.4674L7.04583 22.0593C6.48465 22.238 5.89133 21.8954 5.7655 21.3201L5.34875 19.4145C5.26527 19.0328 4.96716 18.7347 4.58549 18.6513L2.67993 18.2345C2.10458 18.1087 1.76203 17.5154 1.94073 16.9542L2.53259 15.0955C2.65114 14.7233 2.54202 14.316 2.25322 14.0529L0.811331 12.7392C0.375976 12.3426 0.375977 11.6574 0.811332 11.2608L2.25322 9.9471C2.54202 9.68397 2.65114 9.27675 2.53259 8.90447L1.94073 7.04583C1.76203 6.48465 2.10458 5.89133 2.67993 5.7655L4.58549 5.34875C4.96716 5.26527 5.26527 4.96716 5.34875 4.58549L5.7655 2.67993C5.89133 2.10458 6.48465 1.76203 7.04583 1.94073L8.90447 2.53259C9.27675 2.65114 9.68397 2.54202 9.9471 2.25322L11.2608 0.811331Z"
fill="#1A4049" />
<path d="M10.4351 7.3042H7.30469V16.6955H16.696V7.3042H13.5656V13.5651H10.4351V7.3042Z" fill="white" />
<path d="M13.5664 13.5649H16.6968V16.6954H13.5664V13.5649Z" fill="#817AFE" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -20,8 +20,8 @@ import CreateUser from './users/CreateUser/CreateUser';
import EditUser from './users/EditUser/EditUser';
import { InviteLink } from './users/InviteLink/InviteLink';
import UsersAdmin from './users/UsersAdmin';
import { EnterpriseFeatureUpgradePage } from 'component/common/EnterpriseFeatureUpgradePage/EnterpriseFeatureUpgradePage';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
export const Admin = () => {
const { isEnterprise } = useUiConfig();
@ -40,10 +40,7 @@ export const Admin = () => {
isEnterprise() ? (
<ServiceAccounts />
) : (
<EnterpriseFeatureUpgradePage
title="Service accounts"
link="https://docs.getunleash.io/reference/service-accounts"
/>
<PremiumFeature feature="service-accounts" page />
)
}
/>
@ -62,10 +59,7 @@ export const Admin = () => {
isEnterprise() ? (
<Roles />
) : (
<EnterpriseFeatureUpgradePage
title="Project roles"
link="https://docs.getunleash.io/reference/rbac#custom-project-roles"
/>
<PremiumFeature feature="project-roles" page />
)
}
/>

View File

@ -1,5 +1,5 @@
import { useLocation } from 'react-router-dom';
import { Box, Paper, styled, Tab, Tabs } from '@mui/material';
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';
@ -43,7 +43,7 @@ export const AdminTabsMenu: VFC = () => {
link: '/admin/service-accounts',
condition:
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
showEnterpriseBadge: true,
showEnterpriseBadge: isPro(),
},
{
value: 'groups',
@ -57,7 +57,7 @@ export const AdminTabsMenu: VFC = () => {
link: '/admin/roles',
condition:
isEnterprise() || (isPro() && showEnterpriseFeaturesInPro),
showEnterpriseBadge: true,
showEnterpriseBadge: isPro(),
},
{
value: 'api',

View File

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

View File

@ -1,64 +0,0 @@
import { VFC } from 'react';
import { Box, Button, Typography, styled } from '@mui/material';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import { PageContent } from '../PageContent/PageContent';
import { PageHeader } from '../PageHeader/PageHeader';
type EnterpriseFeatureUpgradePageProps = {
title: string;
link: string;
};
const StyledContainer = styled(Box)(({ theme }) => ({
background: theme.palette.background.elevation2,
padding: theme.spacing(8, 2),
borderRadius: `${theme.shape.borderRadiusMedium}px`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}));
const StyledHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(3),
display: 'flex',
alignItems: 'center',
}));
const StyledBadgeContainer = styled('div')(({ theme }) => ({
paddingRight: theme.spacing(1),
display: 'flex',
alignItems: 'center',
}));
export const EnterpriseFeatureUpgradePage: VFC<
EnterpriseFeatureUpgradePageProps
> = ({ title, link }) => (
<PageContent header={<PageHeader title={title} />}>
<StyledContainer>
<StyledHeader variant="h2">
<StyledBadgeContainer>
<EnterpriseBadge size={24} />
</StyledBadgeContainer>
Enterprise feature
</StyledHeader>
<Typography variant="body1">
<a href={link}>{title}</a> is a feature available for the
Enterprise plan.
</Typography>
<Typography variant="body1">
You need to upgrade your plan if you want to use it.
</Typography>
<Button
component="a"
href="https://www.getunleash.io/plans/enterprise"
target="_blank"
rel="noopener noreferrer"
variant="contained"
sx={theme => ({ marginTop: theme.spacing(5) })}
>
Upgrade now
</Button>
</StyledContainer>
</PageContent>
);

View File

@ -4,6 +4,8 @@ import { Box, Button, Link, styled, Typography } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import { ThemeMode } from '../ThemeMode/ThemeMode';
import { PageContent } from '../PageContent/PageContent';
import { PageHeader } from '../PageHeader/PageHeader';
const PremiumFeatureWrapper = styled(Box, {
shouldForwardProp: prop => prop !== 'tooltip',
@ -70,6 +72,21 @@ const PremiumFeatures = {
url: 'https://docs.getunleash.io/reference/segments',
label: 'Segments',
},
'service-accounts': {
plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/service-accounts',
label: 'Service Accounts',
},
'project-roles': {
plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/rbac#custom-project-roles',
label: 'Project Roles',
},
'login-history': {
plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/login-history',
label: 'Login history',
},
};
type PremiumFeatureType = keyof typeof PremiumFeatures;
@ -79,9 +96,14 @@ const UPGRADE_URL = 'https://www.getunleash.io/plans';
export interface PremiumFeatureProps {
feature: PremiumFeatureType;
tooltip?: boolean;
page?: boolean;
}
export const PremiumFeature = ({ feature, tooltip }: PremiumFeatureProps) => {
export const PremiumFeature = ({
feature,
tooltip,
page,
}: PremiumFeatureProps) => {
const { url, plan, label } = PremiumFeatures[feature];
const tracker = usePlausibleTracker();
@ -110,7 +132,7 @@ export const PremiumFeature = ({ feature, tooltip }: PremiumFeatureProps) => {
const upgradeUrl = `${UPGRADE_URL}?feature=${feature}`;
return (
const content = (
<PremiumFeatureWrapper tooltip={tooltip}>
<StyledTitle>
<ThemeMode
@ -168,4 +190,14 @@ export const PremiumFeature = ({ feature, tooltip }: PremiumFeatureProps) => {
/>
</PremiumFeatureWrapper>
);
if (page) {
return (
<PageContent header={<PageHeader title={label} />}>
{content}
</PageContent>
);
}
return content;
};

View File

@ -45,12 +45,14 @@ interface IVerticalTabProps {
label: string;
selected?: boolean;
onClick: () => void;
icon?: React.ReactNode;
}
export const VerticalTab = ({
label,
selected,
onClick,
icon,
}: IVerticalTabProps) => (
<StyledTab
selected={Boolean(selected)}
@ -60,5 +62,6 @@ export const VerticalTab = ({
fullWidth
>
{label}
{icon}
</StyledTab>
);

View File

@ -31,6 +31,7 @@ export interface ITab {
label: string;
path?: string;
hidden?: boolean;
icon?: React.ReactNode;
}
interface IVerticalTabsProps {
@ -56,6 +57,7 @@ export const VerticalTabs = ({
label={tab.label}
selected={tab.id === value}
onClick={() => onChange(tab)}
icon={tab.icon}
/>
))}
</StyledTabs>

View File

@ -2,18 +2,13 @@ import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { EnterpriseFeatureUpgradePage } from 'component/common/EnterpriseFeatureUpgradePage/EnterpriseFeatureUpgradePage';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
export const LoginHistory = () => {
const { isEnterprise } = useUiConfig();
if (!isEnterprise()) {
return (
<EnterpriseFeatureUpgradePage
title="Login history"
link="https://docs.getunleash.io/reference/login-history"
/>
);
return <PremiumFeature feature="login-history" page />;
}
return (

View File

@ -121,6 +121,7 @@ export const DrawerMenu: VFC<IDrawerMenuProps> = ({
path={item.path}
text={item.title}
key={item.path}
mode={item.menu?.mode}
/>
))}
</List>

View File

@ -38,6 +38,8 @@ 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';
const StyledHeader = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@ -121,9 +123,13 @@ const Header: VFC = () => {
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
const { uiConfig, isOss, isPro, isEnterprise } = useUiConfig();
const { isBilling } = useInstanceStatus();
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);
@ -131,26 +137,6 @@ const Header: VFC = () => {
const routes = getRoutes();
const filterByMode = (route: INavigationMenuItem): boolean => {
const { mode, showEnterpriseBadge } = route.menu;
if (!mode) return true;
if (isPro()) {
return (
mode.includes('pro') ||
(mode.includes('enterprise') && showEnterpriseBadge) ||
false
);
}
if (isEnterprise()) {
return mode.includes('enterprise');
}
return false;
};
const filteredMainRoutes = {
mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes)
.concat(
@ -182,7 +168,17 @@ const Header: VFC = () => {
.map(mapRouteLink),
adminRoutes: adminMenuRoutes
.filter(filterByConfig(uiConfig))
.filter(filterByMode)
.filter(route =>
filterAdminRoutes(
route?.menu,
{
enterprise: isEnterprise(),
pro: isPro(),
billing: isBilling,
},
showEnterpriseOptionsInPro
)
)
.map(mapRouteLink),
};

View File

@ -1,9 +1,14 @@
import { ListItem, Link, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { INavigationMenuItem } from 'interfaces/route';
import { Link as RouterLink } from 'react-router-dom';
interface INavigationLinkProps {
path: string;
text: string;
handleClose: () => void;
mode?: INavigationMenuItem['menu']['mode'];
}
const StyledListItem = styled(ListItem)({
@ -37,7 +42,25 @@ const StyledSpan = styled('span')(({ theme }) => ({
borderRadius: '2px',
}));
const NavigationLink = ({ path, text, handleClose }: INavigationLinkProps) => {
const StyledBadgeContainer = styled('div')(({ theme }) => ({
marginLeft: 'auto',
paddingLeft: theme.spacing(2),
display: 'flex',
}));
const NavigationLink = ({
path,
text,
handleClose,
...props
}: INavigationLinkProps) => {
const { uiConfig, isPro } = useUiConfig();
const showEnterpriseBadgeToPro = Boolean(
uiConfig?.flags?.frontendNavigationUpdate &&
isPro() &&
props.mode?.includes('enterprise')
);
return (
<StyledListItem
onClick={() => {
@ -52,6 +75,15 @@ const NavigationLink = ({ path, text, handleClose }: INavigationLinkProps) => {
>
<StyledSpan />
{text}
<ConditionallyRender
condition={showEnterpriseBadgeToPro}
show={
<StyledBadgeContainer>
<EnterpriseBadge />
</StyledBadgeContainer>
}
/>
</Link>
</StyledListItem>
);

View File

@ -5,6 +5,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { INavigationMenuItem } from 'interfaces/route';
import { Link } from 'react-router-dom';
import { EnterpriseBadge } from '../../../common/EnterpriseBadge/EnterpriseBadge';
import { useCallback } from 'react';
interface INavigationMenuProps {
options: INavigationMenuItem[];
@ -49,9 +50,25 @@ export const NavigationMenu = ({
anchorEl,
style,
}: INavigationMenuProps) => {
const { uiConfig } = useUiConfig();
const { uiConfig, isPro } = useUiConfig();
const showUpdatedMenu = uiConfig?.flags?.frontendNavigationUpdate;
const showBadge = useCallback(
(mode?: INavigationMenuItem['menu']['mode']) => {
if (
isPro() &&
!mode?.includes('pro') &&
mode?.includes('enterprise') &&
showUpdatedMenu
) {
return true;
}
return false;
},
[isPro, showUpdatedMenu]
);
return (
<Menu
id={id}
@ -61,38 +78,36 @@ export const NavigationMenu = ({
style={style}
>
{options
.map((option, i) => [
<ConditionallyRender
key={`${option.path}-divider`}
condition={Boolean(
showUpdatedMenu &&
options[i - 1]?.group &&
options[i - 1]?.group !== option.group
)}
show={<Divider variant="middle" />}
elseShow={null}
/>,
<MenuItem
key={option.path}
component={StyledLink}
to={option.path}
onClick={handleClose}
>
<StyledSpan />
{option.title}
<ConditionallyRender
condition={Boolean(
option.menu.showEnterpriseBadge &&
showUpdatedMenu
)}
show={
<StyledBadgeContainer>
<EnterpriseBadge />
</StyledBadgeContainer>
}
/>
</MenuItem>,
])
.map((option, i) => {
const previousGroup = options[i - 1]?.group;
const addDivider =
showUpdatedMenu &&
previousGroup &&
previousGroup !== option.group;
return [
addDivider ? (
<Divider variant="middle" key={option.group} />
) : null,
<MenuItem
key={option.path}
component={StyledLink}
to={option.path}
onClick={handleClose}
>
<StyledSpan />
{option.title}
<ConditionallyRender
condition={showBadge(option?.menu?.mode)}
show={
<StyledBadgeContainer>
<EnterpriseBadge />
</StyledBadgeContainer>
}
/>
</MenuItem>,
];
})
.flat()
.filter(Boolean)}
</Menu>

View File

@ -0,0 +1,103 @@
import { filterAdminRoutes } from './filterAdminRoutes';
describe('filterAdminRoutes - open souce routes', () => {
test('open source - should show menu item if mode paid plan mode is not defined', () => {
expect(
filterAdminRoutes(
{},
{
pro: false,
enterprise: false,
billing: false,
}
)
).toBe(true);
});
test('open source - should not show menu item from paid plans', () => {
const state = {
pro: false,
enterprise: false,
billing: false,
};
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(false);
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(false);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
false
);
expect(filterAdminRoutes({ billing: true }, state)).toBe(false);
});
test('pro - should show menu item for pro customers', () => {
const state = {
pro: true,
enterprise: false,
billing: false,
};
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(true);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
true
);
// This is to show enterprise badge in pro mode
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(true);
});
test('enterprise - should show menu item if mode enterprise is defined or mode is undefined', () => {
const state = {
pro: false,
enterprise: true,
billing: false,
};
expect(filterAdminRoutes({ mode: ['enterprise'] }, state)).toBe(true);
expect(filterAdminRoutes({ mode: ['pro', 'enterprise'] }, state)).toBe(
true
);
expect(filterAdminRoutes({ mode: ['pro'] }, state)).toBe(false);
});
test('billing - should show menu item if billing is defined', () => {
expect(
filterAdminRoutes(
{ mode: ['pro'], billing: true },
{
pro: true,
enterprise: false,
billing: true,
}
)
).toBe(true);
expect(
filterAdminRoutes(
{ mode: ['enterprise'], billing: true },
{
pro: false,
enterprise: true,
billing: true,
}
)
).toBe(true);
expect(
filterAdminRoutes(
{ mode: ['pro', 'enterprise'], billing: true },
{
pro: true,
enterprise: false,
billing: true,
}
)
).toBe(true);
expect(
filterAdminRoutes(
{ mode: ['pro'], billing: true },
{
pro: false,
enterprise: false,
billing: true,
}
)
).toBe(false);
});
});

View File

@ -0,0 +1,34 @@
import { INavigationMenuItem } from 'interfaces/route';
export const filterAdminRoutes = (
menu: INavigationMenuItem['menu'],
{
pro,
enterprise,
billing,
}: { pro?: boolean; enterprise?: boolean; billing?: boolean },
showEnterpriseOptionsInPro = true
): boolean => {
const mode = menu?.mode;
if (menu?.billing && !billing) return false;
if (!mode || mode.length === 0) {
return true;
}
if (pro) {
if (mode.includes('pro')) {
return true;
}
if (showEnterpriseOptionsInPro && mode.includes('enterprise')) {
return true;
}
}
if (enterprise && mode.includes('enterprise')) {
return true;
}
return false;
};

View File

@ -465,7 +465,6 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
menu: {
adminSettings: true,
mode: ['enterprise'],
showEnterpriseBadge: true,
},
group: 'users',
},
@ -475,7 +474,6 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
menu: {
adminSettings: true,
mode: ['enterprise'],
showEnterpriseBadge: true,
},
flag: UG,
group: 'users',
@ -486,7 +484,6 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
menu: {
adminSettings: true,
mode: ['enterprise'],
showEnterpriseBadge: true,
},
group: 'users',
},
@ -538,7 +535,7 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
{
path: '/admin/admin-invoices',
title: 'Billing & invoices',
menu: { adminSettings: true, mode: ['pro'] },
menu: { adminSettings: true, mode: ['pro'], billing: true },
group: 'instance',
},
{
@ -547,7 +544,6 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
menu: {
adminSettings: true,
mode: ['enterprise'],
showEnterpriseBadge: true,
},
group: 'log',
},

View File

@ -14,7 +14,7 @@ import {
StyledTabContainer,
StyledTopRow,
} from './Project.styles';
import { Paper, Tabs, Typography } from '@mui/material';
import { Box, Paper, Tabs, Typography } from '@mui/material';
import { Delete, Edit, FileUpload } from '@mui/icons-material';
import useToast from 'hooks/useToast';
import useQueryParams from 'hooks/useQueryParams';
@ -40,6 +40,7 @@ import { ProjectSettings } from './ProjectSettings/ProjectSettings';
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
import { ImportModal } from './Import/ImportModal';
import { IMPORT_BUTTON } from 'utils/testIds';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
const NAVIGATE_TO_EDIT_PROJECT = 'NAVIGATE_TO_EDIT_PROJECT';
@ -52,13 +53,15 @@ export const Project = () => {
const [modalOpen, setModalOpen] = useState(false);
const navigate = useNavigate();
const { pathname } = useLocation();
const { isOss, uiConfig } = useUiConfig();
const { isOss, uiConfig, isPro } = useUiConfig();
const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId;
const { favorite, unfavorite } = useFavoriteProjectsApi();
const [showDelDialog, setShowDelDialog] = useState(false);
const updatedNavigation = uiConfig?.flags?.frontendNavigationUpdate;
const tabs = [
{
title: 'Overview',
@ -79,18 +82,34 @@ export const Project = () => {
title: 'Change requests',
path: `${basePath}/change-requests`,
name: 'change-request',
isEnterprise: true,
},
{
title: 'Project settings',
path: `${basePath}/settings`,
name: 'settings',
},
{
title: 'Event log',
path: `${basePath}/logs`,
name: 'logs',
},
];
...(updatedNavigation
? [
{
title: 'Event log',
path: `${basePath}/logs`,
name: 'logs',
},
{
title: 'Project settings',
path: `${basePath}/settings`,
name: 'settings',
},
]
: [
{
title: 'Project settings',
path: `${basePath}/settings`,
name: 'settings',
},
{
title: 'Event log',
path: `${basePath}/logs`,
name: 'logs',
},
]),
].filter(tab => !updatedNavigation || !(isOss() && tab.isEnterprise));
const activeTab = [...tabs]
.reverse()
@ -130,6 +149,17 @@ export const Project = () => {
refetch();
};
const enterpriseIcon = (
<Box
sx={theme => ({
marginLeft: theme.spacing(1),
display: 'flex',
})}
>
<EnterpriseBadge />
</Box>
);
return (
<div ref={ref}>
<StyledHeader>
@ -227,6 +257,16 @@ export const Project = () => {
value={tab.path}
onClick={() => navigate(tab.path)}
data-testid={`TAB_${tab.title}`}
iconPosition={
tab.isEnterprise ? 'end' : undefined
}
icon={
tab.isEnterprise &&
isPro() &&
updatedNavigation
? enterpriseIcon
: undefined
}
/>
))}
</Tabs>

View File

@ -14,12 +14,16 @@ import { ProjectSegments } from './ProjectSegments/ProjectSegments';
import { ProjectDefaultStrategySettings } from './ProjectDefaultStrategySettings/ProjectDefaultStrategySettings';
import { Settings } from './Settings/Settings';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
import { Box } from '@mui/material';
export const ProjectSettings = () => {
const location = useLocation();
const { uiConfig } = useUiConfig();
const { uiConfig, isPro, isEnterprise } = useUiConfig();
const navigate = useNavigate();
const updatedNavigation = uiConfig.flags?.frontendNavigationUpdate;
const tabs: ITab[] = [
...(uiConfig.flags.newProjectLayout
? [
@ -33,18 +37,28 @@ export const ProjectSettings = () => {
id: 'environments',
label: 'Environments',
},
{
id: 'access',
label: 'Access',
},
{
id: 'segments',
label: 'Segments',
},
{
id: 'change-requests',
label: 'Change request configuration',
},
...(!updatedNavigation || isPro() || isEnterprise()
? [
{
id: 'access',
label: 'Access',
},
{
id: 'segments',
label: 'Segments',
},
{
id: 'change-requests',
label: 'Change request configuration',
icon:
isPro() && updatedNavigation ? (
<Box sx={{ marginLeft: 'auto' }}>
<EnterpriseBadge />
</Box>
) : undefined,
},
]
: []),
{
id: 'api-access',
label: 'API access',

View File

@ -30,5 +30,5 @@ interface IRouteMenu {
advanced?: boolean;
adminSettings?: boolean;
mode?: Array<'pro' | 'enterprise'>;
showEnterpriseBadge?: boolean;
billing?: boolean;
}