Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { screen } from '@testing-library/react';
import React from 'react';

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

import { DefaultEntityAccess } from './DefaultEntityAccess';

const queryMocks = vi.hoisted(() => ({
useAllAccountEntities: vi.fn().mockReturnValue({}),
useParams: vi.fn().mockReturnValue({}),
useSearch: vi.fn().mockReturnValue({}),
useGetDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}),
useIsDefaultDelegationRolesForChildAccount: vi
.fn()
.mockReturnValue({ isDefaultDelegationRolesForChildAccount: true }),
}));

vi.mock('src/features/IAM/hooks/useDelegationRole', () => ({
useIsDefaultDelegationRolesForChildAccount:
queryMocks.useIsDefaultDelegationRolesForChildAccount,
}));

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual<any>('@linode/queries');

Check warning on line 24 in packages/manager/src/features/IAM/Roles/Defaults/DefaultEntityAccess.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":24,"column":40,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":24,"endColumn":43,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[835,838],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[835,838],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
return {
...actual,
useGetDefaultDelegationAccessQuery:
queryMocks.useGetDefaultDelegationAccessQuery,
};
});

vi.mock('src/queries/entities/entities', async () => {
const actual = await vi.importActual('src/queries/entities/entities');
return {
...actual,
useAllAccountEntities: queryMocks.useAllAccountEntities,
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
...actual,
useParams: queryMocks.useParams,
useSearch: queryMocks.useSearch,
};
});

describe('DefaultEntityAccess', () => {
it('should render', async () => {
renderWithTheme(<DefaultEntityAccess />);

expect(
screen.getByText('Default Entity Access for Delegate Users')
).toBeVisible();
expect(screen.getByPlaceholderText('Search')).toBeVisible();
expect(screen.getByPlaceholderText('All Entities')).toBeVisible();
expect(screen.getByRole('table')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Paper, Stack, Typography } from '@linode/ui';
import * as React from 'react';

import { AssignedEntitiesTable } from '../../Shared/AssignedEntitiesTable/AssignedEntitiesTable';

export const DefaultEntityAccess = () => {
return (
<Paper>
<Stack>
<Stack marginBottom={2.5}>
<Typography variant="h2">
Default Entity Access for Delegate Users
</Typography>
Expand All @@ -15,6 +17,7 @@ export const DefaultEntityAccess = () => {
the assignment.
</Typography>
</Stack>
<AssignedEntitiesTable />
</Paper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { accountEntityFactory } from 'src/factories/accountEntities';
import { userRolesFactory } from 'src/factories/userRoles';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { AssignedEntitiesTable } from '../../Users/UserEntities/AssignedEntitiesTable';
import { AssignedEntitiesTable } from '../../Shared/AssignedEntitiesTable/AssignedEntitiesTable';

const queryMocks = vi.hoisted(() => ({
useAllAccountEntities: vi.fn().mockReturnValue({}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useUserRoles } from '@linode/queries';
import {
useGetDefaultDelegationAccessQuery,
useUserRoles,
} from '@linode/queries';
import { Select, Typography, useTheme } from '@linode/ui';
import Grid from '@mui/material/Grid';
import { useParams, useSearch } from '@tanstack/react-router';
import { useSearch } from '@tanstack/react-router';
import React from 'react';

import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
Expand All @@ -20,19 +23,23 @@ import { TableSortCell } from 'src/components/TableSortCell';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
import { useAllAccountEntities } from 'src/queries/entities/entities';

import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
import { usePermissions } from '../../hooks/usePermissions';
import { ENTITIES_TABLE_PREFERENCE_KEY } from '../../Shared/constants';
import { RemoveAssignmentConfirmationDialog } from '../../Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog';
import {
addEntityNamesToRoles,
getSearchableFields,
} from '../../Users/UserEntities/utils';
import { ENTITIES_TABLE_PREFERENCE_KEY } from '../constants';
import { RemoveAssignmentConfirmationDialog } from '../RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog';
import {
getFilteredRoles,
getFormattedEntityType,
groupAccountEntitiesByType,
mapEntityTypesForSelect,
} from '../../Shared/utilities';
} from '../utilities';
import { ChangeRoleForEntityDrawer } from './ChangeRoleForEntityDrawer';
import { addEntityNamesToRoles, getSearchableFields } from './utils';

import type { DrawerModes, EntitiesRole } from '../../Shared/types';
import type { DrawerModes, EntitiesRole } from '../types';
import type { EntityType } from '@linode/api-v4';
import type { SelectOption } from '@linode/ui';
import type { Action } from 'src/components/ActionMenu/ActionMenu';
Expand All @@ -44,13 +51,17 @@ const ALL_ENTITIES_OPTION: SelectOption = {

type OrderByKeys = 'entity_name' | 'entity_type' | 'role_name';

export const AssignedEntitiesTable = () => {
const { username } = useParams({
from: '/iam/users/$username',
});
interface Props {
username?: string;
}

export const AssignedEntitiesTable = ({ username }: Props) => {
const theme = useTheme();
const { data: permissions } = usePermissions('account', ['is_account_admin']);

const { isDefaultDelegationRolesForChildAccount } =
useIsDefaultDelegationRolesForChildAccount();

const { selectedRole: selectedRoleSearchParam } = useSearch({
strict: false,
});
Expand All @@ -59,7 +70,9 @@ export const AssignedEntitiesTable = () => {
const [orderBy, setOrderBy] = React.useState<OrderByKeys>('entity_name');

const pagination = usePaginationV2({
currentRoute: '/iam/users/$username/entities',
currentRoute: isDefaultDelegationRolesForChildAccount
? '/iam/roles/defaults/entity-access'
: `/iam/users/$username/entities`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change needed, but eventually we may just allow location.pathname here.. It was done this way because location.pathname isn't type strict as the other router utils, but it does make sense to have it set as such

initialPage: 1,
preferenceKey: ENTITIES_TABLE_PREFERENCE_KEY,
});
Expand Down Expand Up @@ -93,10 +106,30 @@ export const AssignedEntitiesTable = () => {
} = useAllAccountEntities({});

const {
data: assignedRoles,
error: assignedRolesError,
isLoading: assignedRolesLoading,
} = useUserRoles(username ?? '');
data: assignedUserRoles,
error: assignedUserRolesError,
isLoading: assignedUserRolesLoading,
} = useUserRoles(username ?? '', !isDefaultDelegationRolesForChildAccount);

const {
data: delegateDefaultRoles,
error: delegateDefaultRolesError,
isLoading: delegateDefaultRolesLoading,
} = useGetDefaultDelegationAccessQuery({
enabled: isDefaultDelegationRolesForChildAccount,
});

const assignedRoles = isDefaultDelegationRolesForChildAccount
? delegateDefaultRoles
: assignedUserRoles;

const error = isDefaultDelegationRolesForChildAccount
? delegateDefaultRolesError
: assignedUserRolesError;

const loading = isDefaultDelegationRolesForChildAccount
? delegateDefaultRolesLoading
: assignedUserRolesLoading;

const { filterableOptions, roles } = React.useMemo(() => {
if (!assignedRoles || !entities) {
Expand Down Expand Up @@ -158,11 +191,11 @@ export const AssignedEntitiesTable = () => {
});

const renderTableBody = () => {
if (entitiesLoading || assignedRolesLoading) {
if (entitiesLoading || loading) {
return <TableRowLoading columns={3} rows={1} />;
}

if (entitiesError || assignedRolesError) {
if (entitiesError || error) {
return (
<TableRowError
colSpan={3}
Expand Down Expand Up @@ -321,11 +354,13 @@ export const AssignedEntitiesTable = () => {
onClose={() => setIsChangeRoleForEntityDrawerOpen(false)}
open={isChangeRoleForEntityDrawerOpen}
role={selectedRole}
username={username}
/>
<RemoveAssignmentConfirmationDialog
onClose={() => handleRemoveAssignmentDialogClose()}
open={isRemoveAssignmentDialogOpen}
role={selectedRole}
username={username}
/>
{filteredRoles.length > PAGE_SIZES[0] && (
<PaginationFooter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { accountRolesFactory } from 'src/factories/accountRoles';
import { userRolesFactory } from 'src/factories/userRoles';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { ChangeRoleForEntityDrawer } from './ChangeRoleForEntityDrawer';
import { ChangeRoleForEntityDrawer } from '../../Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer';

import type { EntitiesRole } from '../../Shared/types';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
useAccountRoles,
useGetDefaultDelegationAccessQuery,
useUpdateDefaultDelegationAccessQuery,
useUserRoles,
useUserRolesMutation,
} from '@linode/queries';
Expand All @@ -11,61 +13,99 @@ import {
Typography,
} from '@linode/ui';
import { useTheme } from '@mui/material/styles';
import { useParams } from '@tanstack/react-router';
import React from 'react';
import { Controller, useForm } from 'react-hook-form';

import { Link } from 'src/components/Link';

import { AssignedPermissionsPanel } from '../../Shared/AssignedPermissionsPanel/AssignedPermissionsPanel';
import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
import { AssignedPermissionsPanel } from '../AssignedPermissionsPanel/AssignedPermissionsPanel';
import {
INTERNAL_ERROR_NO_CHANGES_SAVED,
ROLES_LEARN_MORE_LINK,
} from '../../Shared/constants';
} from '../constants';
import {
changeRoleForEntity,
getAllRoles,
getRoleByName,
} from '../../Shared/utilities';
isAccountRole,
isEntityRole,
} from '../utilities';

import type { DrawerModes, EntitiesRole } from '../../Shared/types';
import type { ExtendedEntityRole } from '../../Shared/utilities';
import type { DrawerModes, EntitiesRole } from '../types';
import type { ExtendedEntityRole } from '../utilities';

interface Props {
mode: DrawerModes;
onClose: () => void;
open: boolean;
role: EntitiesRole | undefined;
username?: string;
}

export const ChangeRoleForEntityDrawer = ({
mode,
onClose,
open,
role,
username,
}: Props) => {
const theme = useTheme();
const { username } = useParams({
from: '/iam/users/$username',
});

const { isDefaultDelegationRolesForChildAccount } =
useIsDefaultDelegationRolesForChildAccount();

const { data: accountRoles, isLoading: accountPermissionsLoading } =
useAccountRoles();

const { data: assignedRoles } = useUserRoles(username ?? '');
const { data: assignedUserRoles } = useUserRoles(
username ?? '',
!isDefaultDelegationRolesForChildAccount
);

const { data: delegateDefaultRoles } = useGetDefaultDelegationAccessQuery({
enabled: isDefaultDelegationRolesForChildAccount,
});

const assignedRoles = isDefaultDelegationRolesForChildAccount
? delegateDefaultRoles
: assignedUserRoles;

const { mutateAsync: updateUserRoles } = useUserRolesMutation(username ?? '');

const { mutateAsync: updateUserRoles } = useUserRolesMutation(username);
const { mutateAsync: updateDefaultDelegationRoles } =
useUpdateDefaultDelegationAccessQuery();

// filtered roles by entity_type and access
const allRoles = React.useMemo(() => {
if (!accountRoles) {
return [];
}

return getAllRoles(accountRoles).filter(
(el) => el.entity_type === role?.entity_type && el.access === role?.access
);
}, [accountRoles, role]);
return getAllRoles(accountRoles).filter((el) => {
const matchesRoleContext =
el.entity_type === role?.entity_type &&
el.access === role?.access &&
el.value !== role?.role_name;

// Exclude account roles already assigned to the user
if (isAccountRole(el)) {
return (
!assignedRoles?.account_access.includes(el.value) &&
matchesRoleContext
);
}
// Exclude entity roles already assigned to the user
if (isEntityRole(el)) {
return (
!assignedRoles?.entity_access.some((entity) =>
entity.roles.includes(el.value)
) && matchesRoleContext
);
}
return true;
});
}, [accountRoles, role, assignedRoles]);

const {
control,
Expand Down Expand Up @@ -93,6 +133,10 @@ export const ChangeRoleForEntityDrawer = ({
return getRoleByName(accountRoles, selectedOptions.value);
}, [selectedOptions, accountRoles]);

const mutationFn = isDefaultDelegationRolesForChildAccount
? updateDefaultDelegationRoles
: updateUserRoles;

const onSubmit = async (data: { roleName: ExtendedEntityRole }) => {
if (role?.role_name === data.roleName.label) {
handleClose();
Expand All @@ -112,7 +156,7 @@ export const ChangeRoleForEntityDrawer = ({
newRole
);

await updateUserRoles({
await mutationFn({
...assignedRoles!,
entity_access: updatedEntityRoles,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ export const AssignedRolesTable = () => {
onClose={() => setIsRemoveAssignmentDialogOpen(false)}
open={isRemoveAssignmentDialogOpen}
role={selectedRoleDetails}
username={username}
/>
{filteredAndSortedRolesCount > PAGE_SIZES[0] && (
<PaginationFooter
Expand Down
Loading