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 (
+ }>
+
+
+
+
+
+
+
+ );
+};