From b1897654e13c4b491108fa75ea4993e7454ac87c Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Fri, 22 Nov 2024 15:24:50 +0100 Subject: [PATCH] feat: [UIE-8131] - RBAC-1: Routes, Menu, Feature Flag --- .../pr-11310-added-1732287073605.md | 5 ++ packages/manager/src/MainContent.tsx | 12 +++ .../src/assets/icons/entityIcons/iam.svg | 3 + .../src/components/PrimaryNav/PrimaryNav.tsx | 12 +++ .../manager/src/dev-tools/FeatureFlagTool.tsx | 1 + packages/manager/src/featureFlags.ts | 2 + .../manager/src/features/IAM/IAMLanding.tsx | 84 +++++++++++++++++++ .../manager/src/features/IAM/Roles/Roles.tsx | 9 ++ .../src/features/IAM/Shared/constants.ts | 4 + .../src/features/IAM/Shared/utilities.ts | 18 ++++ .../features/IAM/Users/UserDetailsLanding.tsx | 83 ++++++++++++++++++ .../manager/src/features/IAM/Users/Users.tsx | 34 ++++++++ packages/manager/src/features/IAM/index.tsx | 34 ++++++++ 13 files changed, 301 insertions(+) create mode 100644 packages/manager/.changeset/pr-11310-added-1732287073605.md create mode 100644 packages/manager/src/assets/icons/entityIcons/iam.svg create mode 100644 packages/manager/src/features/IAM/IAMLanding.tsx create mode 100644 packages/manager/src/features/IAM/Roles/Roles.tsx create mode 100644 packages/manager/src/features/IAM/Shared/constants.ts create mode 100644 packages/manager/src/features/IAM/Shared/utilities.ts create mode 100644 packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx create mode 100644 packages/manager/src/features/IAM/Users/Users.tsx create mode 100644 packages/manager/src/features/IAM/index.tsx diff --git a/packages/manager/.changeset/pr-11310-added-1732287073605.md b/packages/manager/.changeset/pr-11310-added-1732287073605.md new file mode 100644 index 00000000000..1898b1f4523 --- /dev/null +++ b/packages/manager/.changeset/pr-11310-added-1732287073605.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +New routes for iam, feature flag and menu item ([#11310](https://github.com/linode/manager/pull/11310)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 225b80692f9..da9e4cd0d6b 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -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: { @@ -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(); @@ -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, @@ -346,6 +355,9 @@ export const MainContent = () => { path="/object-storage" /> + {isIAMEnabled && ( + + )} diff --git a/packages/manager/src/assets/icons/entityIcons/iam.svg b/packages/manager/src/assets/icons/entityIcons/iam.svg new file mode 100644 index 00000000000..6fc8bf9b920 --- /dev/null +++ b/packages/manager/src/assets/icons/entityIcons/iam.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index fd6b10de5c5..9aac1445907 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -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'; @@ -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' @@ -42,6 +44,7 @@ export type NavEntity = | 'Domains' | 'Firewalls' | 'Help & Support' + | 'Identity and Access' | 'Images' | 'Kubernetes' | 'Linodes' @@ -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(); @@ -210,6 +215,13 @@ export const PrimaryNav = (props: PrimaryNavProps) => { hide: !flags.selfServeBetas, href: '/betas', }, + { + display: 'Identity and Access', + hide: !isIAMEnabled, + href: '/iam', + icon: , + isBeta: isIAMBeta, + }, { display: 'Account', href: '/account', diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 55698daeb63..c296330d7f8 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -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 = ( diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index cc039ff04a1..a85bcea09e0 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -110,6 +110,7 @@ export interface Flags { disallowImageUploadToNonObjRegions: boolean; gecko2: GeckoFeatureFlag; gpuv2: gpuV2; + iam: BetaFeatureFlag; imageServiceGen2: boolean; imageServiceGen2Ga: boolean; ipv6Sharing: boolean; @@ -223,6 +224,7 @@ export type ProductInformationBannerLocation = | 'Databases' | 'Domains' | 'Firewalls' + | 'Identity and Access Management' | 'Images' | 'Kubernetes' | 'LinodeCreate' // Use for Marketplace banners diff --git a/packages/manager/src/features/IAM/IAMLanding.tsx b/packages/manager/src/features/IAM/IAMLanding.tsx new file mode 100644 index 00000000000..715755962b6 --- /dev/null +++ b/packages/manager/src/features/IAM/IAMLanding.tsx @@ -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 ( + <> + + + + + + + }> + + + + + + + + + + + + ); +}); diff --git a/packages/manager/src/features/IAM/Roles/Roles.tsx b/packages/manager/src/features/IAM/Roles/Roles.tsx new file mode 100644 index 00000000000..3c365bb8400 --- /dev/null +++ b/packages/manager/src/features/IAM/Roles/Roles.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const RolesLanding = () => { + return ( + <> +

Roles Table - UIE-8142

+ + ); +}; diff --git a/packages/manager/src/features/IAM/Shared/constants.ts b/packages/manager/src/features/IAM/Shared/constants.ts new file mode 100644 index 00000000000..b647a8e3fe0 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/constants.ts @@ -0,0 +1,4 @@ +// Various constants for the IAM package + +// Labels +export const IAM_LABEL = 'Identity and Access'; diff --git a/packages/manager/src/features/IAM/Shared/utilities.ts b/packages/manager/src/features/IAM/Shared/utilities.ts new file mode 100644 index 00000000000..9617efd47c6 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/utilities.ts @@ -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, + }; +}; diff --git a/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx b/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx new file mode 100644 index 00000000000..5e89e1f9908 --- /dev/null +++ b/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx @@ -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 ( + <> + + + + + +

user details - UIE-8137

+
+ +

UIE-8138 - User Roles - Assigned Roles Table

+
+ +

Resources

+
+
+
+ + ); +}; diff --git a/packages/manager/src/features/IAM/Users/Users.tsx b/packages/manager/src/features/IAM/Users/Users.tsx new file mode 100644 index 00000000000..e2fd573d1cf --- /dev/null +++ b/packages/manager/src/features/IAM/Users/Users.tsx @@ -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 ( + <> +

Users Table - UIE-8136

+ + + + ); +}; diff --git a/packages/manager/src/features/IAM/index.tsx b/packages/manager/src/features/IAM/index.tsx new file mode 100644 index 00000000000..b126b018069 --- /dev/null +++ b/packages/manager/src/features/IAM/index.tsx @@ -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 ( + }> + + + + + + + + ); +};