Skip to content

Commit

Permalink
feat: [UIE-8137] - add new user details component
Browse files Browse the repository at this point in the history
  • Loading branch information
aaleksee-akamai committed Dec 11, 2024
1 parent 0bd360b commit c133b46
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 29 deletions.
40 changes: 23 additions & 17 deletions packages/api-v4/src/iam/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
export interface IamUserPermissions {
account_access: AccountAccessType[];
resource_access: ResourceAccess[];
}

type ResourceType =
export type ResourceTypePermissions =
| 'linode'
| 'firewall'
| 'nodebalancer'
Expand All @@ -16,38 +11,49 @@ type ResourceType =
| 'account'
| 'vpc';

type AccountAccessType =
export type AccountAccessType =
| 'account_linode_admin'
| 'linode_creator'
| 'linode_contributor'
| 'firewall_creator';

type RoleType = 'linode_contributor' | 'firewall_admin';
export type RoleType =
| 'linode_contributor'
| 'firewall_admin'
| 'linode_creator'
| 'firewall_creator';

export interface IamUserPermissions {
account_access: AccountAccessType[];
resource_access: ResourceAccess[];
}
export interface ResourceAccess {
resource_id: number;
resource_type: ResourceType;
resource_type: ResourceTypePermissions;
roles: RoleType[];
}

export interface IamAccountPermissions {
account_access: Access[];
resource_access: Access[];
}

type PermissionType =
| 'create_linode'
| 'update_linode'
| 'update_firewall'
| 'delete_linode'
| 'view_linode';

interface Access {
resource_type: ResourceType;
export interface IamAccountPermissions {
account_access: IamAccess[];
resource_access: IamAccess[];
}

export interface IamAccess {
resource_type: ResourceTypePermissions;
roles: Roles[];
}

export interface Roles {
name: string;
description: string;
permissions?: PermissionType[];
permissions: PermissionType[];
}

export type IamAccessType = keyof IamAccountPermissions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add new user details components for iam ([#11397](https://github.com/linode/manager/pull/11397))
18 changes: 11 additions & 7 deletions packages/manager/src/factories/accountPermissions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { IamAccountPermissions } from '@linode/api-v4';
import Factory from 'src/factories/factoryProxy';

import type { IamAccountPermissions } from '@linode/api-v4';

export const accountPermissionsFactory = Factory.Sync.makeFactory<IamAccountPermissions>(
{
account_access: [
{
resource_type: 'account',
roles: [
{
name: 'account_admin',
description:
'Access to perform any supported action on all resources in the account',
name: 'account_admin',
permissions: ['create_linode', 'update_linode', 'update_firewall'],
},
],
Expand All @@ -19,9 +20,9 @@ export const accountPermissionsFactory = Factory.Sync.makeFactory<IamAccountPerm
resource_type: 'linode',
roles: [
{
name: 'account_linode_admin',
description:
'Access to perform any supported action on all linode instances in the account',
name: 'account_linode_admin',
permissions: ['create_linode', 'update_linode', 'delete_linode'],
},
],
Expand All @@ -30,8 +31,9 @@ export const accountPermissionsFactory = Factory.Sync.makeFactory<IamAccountPerm
resource_type: 'firewall',
roles: [
{
name: 'firewall_creator',
description: 'Access to create a firewall instance',
name: 'firewall_creator',
permissions: ['update_firewall'],
},
],
},
Expand All @@ -41,8 +43,8 @@ export const accountPermissionsFactory = Factory.Sync.makeFactory<IamAccountPerm
resource_type: 'linode',
roles: [
{
name: 'linode_contributor',
description: 'Access to update a linode instance',
name: 'linode_contributor',
permissions: ['update_linode', 'view_linode'],
},
],
Expand All @@ -51,13 +53,15 @@ export const accountPermissionsFactory = Factory.Sync.makeFactory<IamAccountPerm
resource_type: 'firewall',
roles: [
{
name: 'firewall_viewer',
description: 'Access to view a firewall instance',
name: 'firewall_viewer',
permissions: ['update_firewall'],
},
{
name: 'firewall_admin',
description:
'Access to perform any supported action on a firewall instance',
name: 'firewall_admin',
permissions: ['update_firewall'],
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Box, Button, Paper, Stack, Typography } from '@linode/ui';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';

import { PARENT_USER } from 'src/features/Account/constants';
import { useProfile } from 'src/queries/profile/profile';

import { UserDeleteConfirmation } from './UserDeleteConfirmation';

import type { User } from '@linode/api-v4';

interface Props {
user: User;
}

export const DeleteUserPanel = ({ user }: Props) => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

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

const isProxyUserProfile = user.user_type === 'proxy';

const tooltipText =
profile?.username === user.username
? 'You can\u{2019}t delete the currently active user.'
: isProxyUserProfile
? `You can\u{2019}t delete a ${PARENT_USER}.`
: undefined;

return (
<Paper>
<Stack spacing={1}>
<Typography variant="h2">Delete User</Typography>
<Box>
<Button
buttonType="outlined"
disabled={profile?.username === user.username || isProxyUserProfile}
onClick={() => setIsDeleteDialogOpen(true)}
tooltipText={tooltipText}
>
Delete
</Button>
</Box>
<Typography variant="body1">
The user will be deleted permanently.
</Typography>
<UserDeleteConfirmation
onClose={() => setIsDeleteDialogOpen(false)}
onSuccess={() => history.push(`/account/users`)}
open={isDeleteDialogOpen}
username={user.username}
/>
</Stack>
</Paper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Notice, Typography } from '@linode/ui';
import { useSnackbar } from 'notistack';
import * as React from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import { useAccountUserDeleteMutation } from 'src/queries/account/users';

interface Props {
onClose: () => void;
onSuccess?: () => void;
open: boolean;
username: string;
}

export const UserDeleteConfirmation = (props: Props) => {
const { onClose: _onClose, onSuccess, open, username } = props;

const { enqueueSnackbar } = useSnackbar();

const {
error,
isPending,
mutateAsync: deleteUser,
reset,
} = useAccountUserDeleteMutation(username);

const onClose = () => {
reset(); // resets the error state of the useMutation
_onClose();
};

const onDelete = async () => {
await deleteUser();
enqueueSnackbar(`User ${username} has been deleted successfully.`, {
variant: 'success',
});
if (onSuccess) {
onSuccess();
}
onClose();
};

return (
<ConfirmationDialog
actions={
<ActionsPanel
primaryButtonProps={{
label: 'Delete User',
loading: isPending,
onClick: onDelete,
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
style={{ padding: 0 }}
/>
}
error={error?.[0].reason}
onClose={onClose}
open={open}
title={`Delete user ${username}?`}
>
<Notice variant="warning">
<Typography sx={{ fontSize: '0.875rem' }}>
<strong>Warning:</strong> Deleting this User is permanent and can’t be
undone.
</Typography>
</Notice>
</ConfirmationDialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';

import { accountUserFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { UserDetailsPanel } from './UserDetailsPanel';

import type { IamUserPermissions } from '@linode/api-v4';

describe('UserDetailsPanel', () => {
it("renders the user's username and email", async () => {
const user = accountUserFactory.build();
const assignedRoles = { account_access: [], resource_access: [] };

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('Username')).toBeVisible();
expect(getByText(user.username)).toBeVisible();

expect(getByText('Email')).toBeVisible();
expect(getByText(user.email)).toBeVisible();
});

it("renders 'no roles assigned' if the user doesn't have the assigned roles", async () => {
const user = accountUserFactory.build({ restricted: true });
const assignedRoles = { account_access: [], resource_access: [] };

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('Access')).toBeVisible();
expect(getByText('no roles assigned')).toBeVisible();
});

it("renders '5 roles assigned' if the user has 5 different roles", async () => {
const user = accountUserFactory.build({ restricted: false });
const assignedRoles: IamUserPermissions = {
account_access: [
'account_linode_admin',
'linode_creator',
'firewall_creator',
],
resource_access: [
{
resource_id: 12345678,
resource_type: 'linode',
roles: ['linode_contributor', 'linode_creator'],
},
{
resource_id: 45678901,
resource_type: 'firewall',
roles: ['firewall_admin', 'firewall_creator'],
},
],
};

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('Access')).toBeVisible();
expect(getByText('5 roles assigned')).toBeVisible();
});

it("renders '3 roles assigned' if the user has 3 different roles", async () => {
const user = accountUserFactory.build({ restricted: false });
const assignedRoles: IamUserPermissions = {
account_access: [
'account_linode_admin',
'linode_creator',
'linode_contributor',
],
resource_access: [
{
resource_id: 12345678,
resource_type: 'linode',
roles: ['linode_contributor', 'linode_creator'],
},
],
};

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('Access')).toBeVisible();
expect(getByText('3 roles assigned')).toBeVisible();
});

it("renders the user's phone number", async () => {
const user = accountUserFactory.build({
verified_phone_number: '+17040000000',
});
const assignedRoles = { account_access: [], resource_access: [] };

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('Verified Phone Number')).toBeVisible();
expect(getByText(user.verified_phone_number!)).toBeVisible();
});

it("renders the user's 2FA status", async () => {
const user = accountUserFactory.build({ tfa_enabled: true });
const assignedRoles = { account_access: [], resource_access: [] };

const { getByText } = renderWithTheme(
<UserDetailsPanel assignedRoles={assignedRoles} user={user} />
);

expect(getByText('2FA')).toBeVisible();
expect(getByText('Enabled')).toBeVisible();
});
});
Loading

0 comments on commit c133b46

Please sign in to comment.