Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [UIE-8131] - IAM RBAC: Routes, Menu, Feature Flag #11310

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
84 changes: 84 additions & 0 deletions packages/manager/src/features/IAM/IAMLanding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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,
}))
);

export 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>
</>
);
});
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').then((module) => ({
default: module.IdentityAccessManagementLanding,
}))
);

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

export const IdentityAccessManagement = (props: RouteComponentProps) => {
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>
);
};
Loading