Skip to content

Commit

Permalink
feat: [M3-7529] – Support VPC in Personal Access Token drawer (#10024)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwiley-akamai authored Jan 17, 2024
1 parent 6561b4a commit b9ce55d
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Support VPC in Access Token drawers ([#10024](https://github.com/linode/manager/pull/10024))
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from 'react';

import Check from 'src/assets/icons/monitor-ok.svg';
import { Radio } from 'src/components/Radio/Radio';
import { Tooltip } from 'src/components/Tooltip';

interface RadioButton extends HTMLInputElement {
name: string;
Expand All @@ -15,11 +16,20 @@ interface AccessCellProps {
onChange: (e: React.SyntheticEvent<RadioButton>) => void;
scope: string;
scopeDisplay: string;
tooltipText?: string;
viewOnly: boolean;
}

export const AccessCell = React.memo((props: AccessCellProps) => {
const { active, disabled, onChange, scope, scopeDisplay, viewOnly } = props;
const {
active,
disabled,
onChange,
scope,
scopeDisplay,
tooltipText,
viewOnly,
} = props;

if (viewOnly) {
if (!active) {
Expand All @@ -36,7 +46,7 @@ export const AccessCell = React.memo((props: AccessCellProps) => {
);
}

return (
const radioBtn = (
<Radio
inputProps={{
'aria-label': `${scope} for ${scopeDisplay}`,
Expand All @@ -49,6 +59,14 @@ export const AccessCell = React.memo((props: AccessCellProps) => {
value={scope}
/>
);

return tooltipText ? (
<Tooltip placement="top" title={tooltipText}>
<span>{radioBtn}</span>
</Tooltip>
) : (
radioBtn
);
});

const StyledCheckIcon = styled('span', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ describe('Create API Token Drawer', () => {
expect(childScope).not.toBeInTheDocument();
});

it('Should show the VPC scope with the VPC feature flag on', () => {
const { getByText } = renderWithTheme(<CreateAPITokenDrawer {...props} />, {
flags: { vpc: true },
});
const vpcScope = getByText('VPCs');
expect(vpcScope).toBeInTheDocument();
});

it('Should not show the VPC scope with the VPC feature flag off', () => {
const { queryByText } = renderWithTheme(
<CreateAPITokenDrawer {...props} />,
{
flags: { vpc: false },
}
);

const vpcScope = queryByText('VPCs');
expect(vpcScope).not.toBeInTheDocument();
});

it('Should close when Cancel is pressed', () => {
const { getByText } = renderWithTheme(<CreateAPITokenDrawer {...props} />);
const cancelButton = getByText(/Cancel/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import { TableRow } from 'src/components/TableRow';
import { TextField } from 'src/components/TextField';
import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants';
import { AccessCell } from 'src/features/ObjectStorage/AccessKeyLanding/AccessCell';
import { VPC_READ_ONLY_TOOLTIP } from 'src/features/VPCs/constants';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account';
import { useAccountUser } from 'src/queries/accountUsers';
import { useProfile } from 'src/queries/profile';
import { useCreatePersonalAccessTokenMutation } from 'src/queries/tokens';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';
import { getErrorMap } from 'src/utilities/errorUtils';

import {
Expand All @@ -29,9 +32,10 @@ import {
StyledSelectCell,
} from './APITokenDrawer.styles';
import {
basePermNameMap as _basePermNameMap,
Permission,
allScopesAreTheSame,
basePermNameMap,
getPermsNameMap,
permTuplesToScopeString,
scopeStringToPermTuples,
} from './utils';
Expand Down Expand Up @@ -94,6 +98,7 @@ export const CreateAPITokenDrawer = (props: Props) => {
};

const { data: profile } = useProfile();
const { data: account } = useAccount();
const { data: user } = useAccountUser(profile?.username ?? '');

const {
Expand All @@ -102,6 +107,19 @@ export const CreateAPITokenDrawer = (props: Props) => {
mutateAsync: createPersonalAccessToken,
} = useCreatePersonalAccessTokenMutation();

const showVPCs = isFeatureEnabled(
'VPCs',
Boolean(flags.vpc),
account?.capabilities ?? []
);

// @TODO VPC: once VPC enters GA, remove _basePermNameMap logic and references.
// Just use the basePermNameMap import directly w/o any manipulation.
const basePermNameMap = getPermsNameMap(_basePermNameMap, {
name: 'vpc',
shouldBeIncluded: showVPCs,
});

const form = useFormik<{
expiry: string;
label: string;
Expand Down Expand Up @@ -270,6 +288,9 @@ export const CreateAPITokenDrawer = (props: Props) => {
if (!basePermNameMap[scopeTup[0]]) {
return null;
}

const scopeIsForVPC = scopeTup[0] === 'vpc';

return (
<TableRow
data-qa-row={basePermNameMap[scopeTup[0]]}
Expand All @@ -293,8 +314,11 @@ export const CreateAPITokenDrawer = (props: Props) => {
parentColumn="Read Only"
>
<AccessCell
tooltipText={
scopeIsForVPC ? VPC_READ_ONLY_TOOLTIP : undefined
}
active={scopeTup[1] === 1}
disabled={false}
disabled={scopeIsForVPC} // "Read Only" is not a valid scope for VPC
onChange={handleScopeChange}
scope="1"
scopeDisplay={scopeTup[0]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const props = {
token,
};

const ariaLabel = 'aria-label';

describe('View API Token Drawer', () => {
it('the token label should be visible', () => {
const { getByText } = renderWithTheme(<ViewAPITokenDrawer {...props} />);
Expand All @@ -43,10 +45,12 @@ describe('View API Token Drawer', () => {
});

it('should show all permissions as read/write with wildcard scopes', () => {
const { getByTestId } = renderWithTheme(<ViewAPITokenDrawer {...props} />);
const { getByTestId } = renderWithTheme(<ViewAPITokenDrawer {...props} />, {
flags: { vpc: true },
});
for (const permissionName of basePerms) {
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
ariaLabel,
`This token has 2 access for ${permissionName}`
);
}
Expand All @@ -55,11 +59,11 @@ describe('View API Token Drawer', () => {
it('should show all permissions as none with no scopes', () => {
const { getByTestId } = renderWithTheme(
<ViewAPITokenDrawer {...props} token={limitedToken} />,
{ flags: { parentChildAccountAccess: false } }
{ flags: { parentChildAccountAccess: false, vpc: true } }
);
for (const permissionName of basePerms) {
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
ariaLabel,
`This token has 0 access for ${permissionName}`
);
}
Expand All @@ -70,13 +74,14 @@ describe('View API Token Drawer', () => {
<ViewAPITokenDrawer
{...props}
token={appTokenFactory.build({ scopes: 'account:read_write' })}
/>
/>,
{ flags: { vpc: true } }
);
for (const permissionName of basePerms) {
// We only expect account to have read/write for this test
const expectedScopeLevel = permissionName === 'account' ? 2 : 0;
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
ariaLabel,
`This token has ${expectedScopeLevel} access for ${permissionName}`
);
}
Expand All @@ -88,9 +93,10 @@ describe('View API Token Drawer', () => {
{...props}
token={appTokenFactory.build({
scopes:
'databases:read_only domains:read_write events:read_write firewall:read_write images:read_write ips:read_write linodes:read_only lke:read_only longview:read_write nodebalancers:read_write object_storage:read_only stackscripts:read_write volumes:read_only',
'databases:read_only domains:read_write events:read_write firewall:read_write images:read_write ips:read_write linodes:read_only lke:read_only longview:read_write nodebalancers:read_write object_storage:read_only stackscripts:read_write volumes:read_only vpc:read_write',
})}
/>
/>,
{ flags: { vpc: true } }
);

const expectedScopeLevels = {
Expand All @@ -108,12 +114,13 @@ describe('View API Token Drawer', () => {
object_storage: 1,
stackscripts: 2,
volumes: 1,
vpc: 2,
} as const;

for (const permissionName of basePerms) {
const expectedScopeLevel = expectedScopeLevels[permissionName];
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
ariaLabel,
`This token has ${expectedScopeLevel} access for ${permissionName}`
);
}
Expand Down Expand Up @@ -145,7 +152,7 @@ describe('View API Token Drawer', () => {

expect(childScope).toBeInTheDocument();
expect(getByTestId(`perm-${childPermissionName}`)).toHaveAttribute(
'aria-label',
ariaLabel,
`This token has ${expectedScopeLevels[childPermissionName]} access for ${childPermissionName}`
);
});
Expand All @@ -162,4 +169,21 @@ describe('View API Token Drawer', () => {
const childScope = queryByText('Child Account Access');
expect(childScope).not.toBeInTheDocument();
});

it('Should show the VPC scope with the VPC feature flag on', () => {
const { getByText } = renderWithTheme(<ViewAPITokenDrawer {...props} />, {
flags: { vpc: true },
});
const vpcScope = getByText('VPCs');
expect(vpcScope).toBeInTheDocument();
});

it('Should not show the VPC scope with the VPC feature flag off', () => {
const { queryByText } = renderWithTheme(<ViewAPITokenDrawer {...props} />, {
flags: { vpc: false },
});

const vpcScope = queryByText('VPCs');
expect(vpcScope).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { AccessCell } from 'src/features/ObjectStorage/AccessKeyLanding/AccessCell';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account';
import { useAccountUser } from 'src/queries/accountUsers';
import { useProfile } from 'src/queries/profile';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import {
StyledAccessCell,
StyledPermissionsCell,
StyledPermsTable,
} from './APITokenDrawer.styles';
import { basePermNameMap, scopeStringToPermTuples } from './utils';
import {
basePermNameMap as _basePermNameMap,
getPermsNameMap,
scopeStringToPermTuples,
} from './utils';

interface Props {
onClose: () => void;
Expand All @@ -30,8 +36,22 @@ export const ViewAPITokenDrawer = (props: Props) => {
const flags = useFlags();

const { data: profile } = useProfile();
const { data: account } = useAccount();
const { data: user } = useAccountUser(profile?.username ?? '');

const showVPCs = isFeatureEnabled(
'VPCs',
Boolean(flags.vpc),
account?.capabilities ?? []
);

// @TODO VPC: once VPC enters GA, remove _basePermNameMap logic and references.
// Just use the basePermNameMap import directly w/o any manipulation.
const basePermNameMap = getPermsNameMap(_basePermNameMap, {
name: 'vpc',
shouldBeIncluded: showVPCs,
});

const allPermissions = scopeStringToPermTuples(token?.scopes ?? '');

// Filter permissions for all users except parent user accounts.
Expand Down
Loading

0 comments on commit b9ce55d

Please sign in to comment.