Skip to content

Commit

Permalink
feat: [UIE-8131] - RBAC-1: Routes, Menu, Feature Flag
Browse files Browse the repository at this point in the history
  • Loading branch information
aaleksee-akamai committed Dec 2, 2024
1 parent 4b3fe8a commit 703a31b
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11310-added-1732287073605.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

New routes for iam, feature flag and menu item ([#11310](https://github.com/linode/manager/pull/11310))
12 changes: 12 additions & 0 deletions packages/manager/src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { migrationRouter } from './routes';

import type { Theme } from '@mui/material/styles';
import type { AnyRouter } from '@tanstack/react-router';
import { useIsIAMEnabled } from './features/IAM/Shared/utilities';

const useStyles = makeStyles()((theme: Theme) => ({
activationWrapper: {
Expand Down Expand Up @@ -196,6 +197,12 @@ const CloudPulse = React.lazy(() =>
}))
);

const IAM = React.lazy(() =>
import('src/features/IAM').then((module) => ({
default: module.IdentityAccessManagement,
}))
);

export const MainContent = () => {
const { classes, cx } = useStyles();
const { data: preferences } = usePreferences();
Expand Down Expand Up @@ -232,6 +239,8 @@ export const MainContent = () => {

const { isACLPEnabled } = useIsACLPEnabled();

const { isIAMEnabled } = useIsIAMEnabled();

/**
* this is the case where the user has successfully completed signup
* but needs a manual review from Customer Support. In this case,
Expand Down Expand Up @@ -346,6 +355,9 @@ export const MainContent = () => {
path="/object-storage"
/>
<Route component={Kubernetes} path="/kubernetes" />
{isIAMEnabled && (
<Route component={IAM} path="/iam" />
)}
<Route component={Account} path="/account" />
<Route component={Profile} path="/profile" />
<Route component={Help} path="/support" />
Expand Down
3 changes: 3 additions & 0 deletions packages/manager/src/assets/icons/entityIcons/iam.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Linode from 'src/assets/icons/entityIcons/linode.svg';
import NodeBalancer from 'src/assets/icons/entityIcons/nodebalancer.svg';
import Longview from 'src/assets/icons/longview.svg';
import More from 'src/assets/icons/more.svg';
import IAM from 'src/assets/icons/entityIcons/iam.svg';
import { useIsACLPEnabled } from 'src/features/CloudPulse/Utils/utils';
import { useIsDatabasesEnabled } from 'src/features/Databases/utilities';
import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
Expand All @@ -31,6 +32,7 @@ import {
import { linkIsActive } from './utils';

import type { PrimaryLink as PrimaryLinkType } from './PrimaryLink';
import { useIsIAMEnabled } from 'src/features/IAM/Shared/utilities';

export type NavEntity =
| 'Account'
Expand All @@ -42,6 +44,7 @@ export type NavEntity =
| 'Domains'
| 'Firewalls'
| 'Help & Support'
| 'Identity and Access'
| 'Images'
| 'Kubernetes'
| 'Linodes'
Expand Down Expand Up @@ -81,6 +84,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
const { isDatabasesEnabled, isDatabasesV2Beta } = useIsDatabasesEnabled();

const { isIAMEnabled, isIAMBeta } = useIsIAMEnabled();

const { data: preferences } = usePreferences();
const { mutateAsync: updatePreferences } = useMutatePreferences();

Expand Down Expand Up @@ -210,6 +215,13 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
hide: !flags.selfServeBetas,
href: '/betas',
},
{
display: 'Identity and Access',
hide: !isIAMEnabled,
href: '/iam',
icon: <IAM />,
isBeta: isIAMBeta,
},
{
display: 'Account',
href: '/account',
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'dbaasV2MonitorMetrics', label: 'Databases V2 Monitor' },
{ flag: 'databaseResize', label: 'Database Resize' },
{ flag: 'apicliButtonCopy', label: 'APICLI Button Copy' },
{ flag: 'iam', label: 'Identity and Access Beta' },
];

const renderFlagItems = (
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface Flags {
disallowImageUploadToNonObjRegions: boolean;
gecko2: GeckoFeatureFlag;
gpuv2: gpuV2;
iam: BetaFeatureFlag;
imageServiceGen2: boolean;
imageServiceGen2Ga: boolean;
ipv6Sharing: boolean;
Expand Down Expand Up @@ -223,6 +224,7 @@ export type ProductInformationBannerLocation =
| 'Databases'
| 'Domains'
| 'Firewalls'
| 'Identity and Access Management'
| 'Images'
| 'Kubernetes'
| 'LinodeCreate' // Use for Marketplace banners
Expand Down
86 changes: 86 additions & 0 deletions packages/manager/src/features/IAM/IAMLanding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from 'react';
import { matchPath } from 'react-router-dom';

import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { LandingHeader } from 'src/components/LandingHeader';
import { SuspenseLoader } from 'src/components/SuspenseLoader';
import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
import { TabLinkList } from 'src/components/Tabs/TabLinkList';
import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';

import type { RouteComponentProps } from 'react-router-dom';
type Props = RouteComponentProps<{}>;

const Users = React.lazy(() =>
import('./Users/Users').then((module) => ({
default: module.UsersLanding,
}))
);

const Roles = React.lazy(() =>
import('./Roles/Roles').then((module) => ({
default: module.RolesLanding,
}))
);

const IdentityAccessManagementLanding = React.memo((props: Props) => {
const tabs = [
{
routeName: `${props.match.url}/users`,
title: 'Users',
},
{
routeName: `${props.match.url}/roles`,
title: 'Roles',
},
];

const navToURL = (index: number) => {
props.history.push(tabs[index].routeName);
};

const getDefaultTabIndex = () => {
const tabChoice = tabs.findIndex((tab) =>
Boolean(matchPath(tab.routeName, { path: location.pathname }))
);

return tabChoice;
};

const landingHeaderProps = {
breadcrumbProps: {
pathname: '/iam',
},
docsLink:
'https://www.linode.com/docs/platform/identity-access-management/',
entity: 'Identity and Access',
title: 'Identity and Access',
};

let idx = 0;

return (
<>
<DocumentTitleSegment segment="Identity and Access" />
<LandingHeader {...landingHeaderProps} />

<Tabs index={getDefaultTabIndex()} onChange={navToURL}>
<TabLinkList tabs={tabs} />

<React.Suspense fallback={<SuspenseLoader />}>
<TabPanels>
<SafeTabPanel index={idx}>
<Users />
</SafeTabPanel>
<SafeTabPanel index={++idx}>
<Roles />
</SafeTabPanel>
</TabPanels>
</React.Suspense>
</Tabs>
</>
);
});

export default IdentityAccessManagementLanding;
9 changes: 9 additions & 0 deletions packages/manager/src/features/IAM/Roles/Roles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export const RolesLanding = () => {
return (
<>
<p>Roles Table - UIE-8142 </p>
</>
);
};
4 changes: 4 additions & 0 deletions packages/manager/src/features/IAM/Shared/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Various constants for the IAM package

// Labels
export const IAM_LABEL = 'Identity and Access';
18 changes: 18 additions & 0 deletions packages/manager/src/features/IAM/Shared/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useFlags } from 'src/hooks/useFlags';

/**
* Hook to determine if the IAM feature should be visible to the user.
* Based on the user's account capability and the feature flag.
*
* @returns {boolean} - Whether the IAM feature is enabled for the current user.
*/
export const useIsIAMEnabled = () => {
const flags = useFlags();

const isIAMEnabled = flags.iam?.enabled;

return {
isIAMEnabled,
isIAMBeta: flags.iam?.beta,
};
};
83 changes: 83 additions & 0 deletions packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import {
useHistory,
useLocation,
useParams,
matchPath,
} from 'react-router-dom';
import { LandingHeader } from 'src/components/LandingHeader';
import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
import { TabLinkList } from 'src/components/Tabs/TabLinkList';
import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { IAM_LABEL } from '../Shared/constants';

export const UserDetailsLanding = () => {
const { username } = useParams<{ username: string }>();
const location = useLocation();
const history = useHistory();

const tabs = [
{
routeName: `/iam/users/${username}/details`,
title: 'User Details',
},
{
routeName: `/iam/users/${username}/roles`,
title: 'Assigned Roles',
},
{
routeName: `/iam/users/${username}/resources`,
title: 'Assigned Resources',
},
];

const navToURL = (index: number) => {
history.push(tabs[index].routeName);
};

const getDefaultTabIndex = () => {
const tabChoice = tabs.findIndex((tab) =>
Boolean(matchPath(tab.routeName, { path: location.pathname }))
);

return tabChoice;
};

let idx = 0;

return (
<>
<LandingHeader
breadcrumbProps={{
crumbOverrides: [
{
label: IAM_LABEL,
position: 1,
},
],
labelOptions: {
noCap: true,
},
pathname: location.pathname,
}}
removeCrumbX={4}
title={username}
/>
<Tabs index={getDefaultTabIndex()} onChange={navToURL}>
<TabLinkList tabs={tabs} />
<TabPanels>
<SafeTabPanel index={idx}>
<p>user details - UIE-8137</p>
</SafeTabPanel>
<SafeTabPanel index={++idx}>
<p>UIE-8138 - User Roles - Assigned Roles Table</p>
</SafeTabPanel>
<SafeTabPanel index={++idx}>
<p>Resources</p>
</SafeTabPanel>
</TabPanels>
</Tabs>
</>
);
};
34 changes: 34 additions & 0 deletions packages/manager/src/features/IAM/Users/Users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu';
import { useProfile } from 'src/queries/profile/profile';

export const UsersLanding = () => {
const history = useHistory();
const { data: profile } = useProfile();

const username = profile?.username;

const actions: Action[] = [
{
onClick: () => {
history.push(`/iam/users/${username}/details`);
},
title: 'View User Details',
},
{
onClick: () => {
history.push(`/iam/users/${username}/roles`);
},
title: 'View User Roles',
},
];

return (
<>
<p>Users Table - UIE-8136 </p>

<ActionMenu actionsList={actions} ariaLabel={`Action menu for user`} />
</>
);
};
34 changes: 34 additions & 0 deletions packages/manager/src/features/IAM/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';

import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner';
import { SuspenseLoader } from 'src/components/SuspenseLoader';

import type { RouteComponentProps } from 'react-router-dom';

const IAMLanding = React.lazy(() => import('./IAMLanding'));

const UserDetails = React.lazy(() =>
import('./Users/UserDetailsLanding').then((module) => ({
default: module.UserDetailsLanding,
}))
);

type CombinedProps = RouteComponentProps;

export const IdentityAccessManagement: React.FC<CombinedProps> = (props) => {
const path = props.match.path;

return (
<React.Suspense fallback={<SuspenseLoader />}>
<ProductInformationBanner bannerLocation="Identity and Access Management" />
<Switch>
<Route component={UserDetails} path={`${path}/users/:username/`} />
<Redirect exact from={path} to={`${path}/users`} />
<Route component={IAMLanding} path={`${path}`} />
</Switch>
</React.Suspense>
);
};

export default IdentityAccessManagement;

0 comments on commit 703a31b

Please sign in to comment.