Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: [M3-6258] - React Query for SSH Keys #8892

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f10928f
wow
bnussman Mar 16, 2023
f9acbc7
phew, got ssh keys somewhat working on linode create
bnussman Mar 16, 2023
fdf5c3c
finish ssh key select
bnussman Mar 17, 2023
745474c
fix types
bnussman Mar 17, 2023
fd88b13
remove `userSSHKeyHoc.ts` completely
bnussman Mar 17, 2023
c6d491f
fix ssh keys landing
bnussman Mar 17, 2023
c47628c
re-wrote `UserSSHKeyPanel.test.tsx`
bnussman Mar 17, 2023
3c7f86b
re-wrote `SSHKeyCreationDrawer.test.tsx`
bnussman Mar 17, 2023
d64c39c
fix last few tests
bnussman Mar 17, 2023
8df6f57
reset creation drawer on success
bnussman Mar 17, 2023
ffc4e57
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 17, 2023
6f74ea0
add ssh key event handler
bnussman Mar 17, 2023
e2335d2
added the ability for users to update ssh keys
bnussman Mar 17, 2023
7fe1e2c
clean up
bnussman Mar 17, 2023
bea9d9b
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 21, 2023
ba9ddfc
feedback from @dwiley-akamai
bnussman Mar 21, 2023
927cb02
migrate to better query key format
bnussman Mar 21, 2023
c48819f
try to fix validation ci
bnussman Mar 22, 2023
3441f87
feedback from @dwiley-akamai
bnussman Mar 22, 2023
8cedae3
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 22, 2023
d8d9df2
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 23, 2023
09be7e1
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 24, 2023
5d25bf3
feedback from @dwiley-akamai
bnussman Mar 24, 2023
2c14486
use old size for gravatar
bnussman Mar 24, 2023
bc648d2
Merge branch 'develop' into M3-6258-react-query-ssh-keys
bnussman Mar 28, 2023
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,16 @@ jobs:
# Create an api-v4 tarball
- run: cd packages/api-v4 && npm pack --pack-destination ../../

# Create an validation tarball
- run: cd packages/validation && npm pack --pack-destination ../../

# Test @linode/api-v4 as an ES Module
- run: mkdir test-sdk-esm && cd test-sdk-esm && npm init es6 -y && npm install ../$(ls ../ | grep "linode-api-v4-.*\.tgz")
- run: mkdir test-sdk-esm && cd test-sdk-esm && npm init es6 -y && npm install ../$(ls ../ | grep "linode-api-v4-.*\.tgz") ../$(ls ../ | grep "linode-validation-.*\.tgz")
- run: cp scripts/validatePackages/sdk-esm.js test-sdk-esm/
- run: cd test-sdk-esm && node ./sdk-esm.js

# Verify @linode/api-v4 as CommonJS
- run: mkdir test-sdk-cjs && cd test-sdk-cjs && npm init -y && npm install ../$(ls ../ | grep "linode-api-v4-.*\.tgz")
- run: mkdir test-sdk-cjs && cd test-sdk-cjs && npm init -y && npm install ../$(ls ../ | grep "linode-api-v4-.*\.tgz") ../$(ls ../ | grep "linode-validation-.*\.tgz")
- run: cp scripts/validatePackages/sdk-commonjs.cjs test-sdk-cjs/
- run: cd test-sdk-cjs && node ./sdk-commonjs.cjs

Expand Down
11 changes: 7 additions & 4 deletions packages/api-v4/src/profile/sshkeys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createSSHKeySchema } from '@linode/validation/lib/profile.schema';
import {
createSSHKeySchema,
updateSSHKeySchema,
} from '@linode/validation/lib/profile.schema';
import { API_ROOT } from '../constants';
import Request, {
setData,
Expand Down Expand Up @@ -57,11 +60,11 @@ export const createSSHKey = (data: { label: string; ssh_key: string }) =>
* @param keyId { number } the ID of the key to be updated.
*
*/
export const updateSSHKey = (keyId: number, data: Partial<SSHKey>) =>
export const updateSSHKey = (keyId: number, data: { label: string }) =>
Request<SSHKey>(
setMethod('DELETE'),
setMethod('PUT'),
setURL(`${API_ROOT}/profile/sshkeys/${keyId}`),
setData(data, createSSHKeySchema)
setData(data, updateSSHKeySchema)
);

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import withPreferences, {
} from './containers/preferences.container';
import { loadScript } from './hooks/useScript';
import { getNextThemeValue } from './utilities/theme';
import { sshKeyEventHandler } from './queries/profile';
import { firewallEventsHandler } from './queries/firewalls';

interface Props {
Expand Down Expand Up @@ -141,8 +142,14 @@ export class App extends React.Component<CombinedProps, State> {
.subscribe(tokenEventHandler);

/*
Send any Token events to the Token events handler in the queries file
Send any SSH Key events to the SSH Key events handler in the queries file
*/
events$
.filter(
(event) => event.action.startsWith('user_ssh_key') && !event._initial
)
.subscribe(sshKeyEventHandler);

events$
.filter((event) => event.action.startsWith('firewall') && !event._initial)
.subscribe(firewallEventsHandler);
Expand Down
27 changes: 0 additions & 27 deletions packages/manager/src/__data__/userSSHKeyObjects.ts

This file was deleted.

28 changes: 7 additions & 21 deletions packages/manager/src/components/AccessPanel/AccessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,11 @@ const styled = withStyles(styles);
interface Props {
password: string | null;
error?: string;
sshKeyError?: string;
handleChange: (value: string) => void;
heading?: string;
label?: string;
required?: boolean;
placeholder?: string;
users?: UserSSHKeyObject[];
requestKeys?: () => void;
disabled?: boolean;
disabledReason?: string | JSX.Element;
tooltipInteractive?: boolean;
Expand All @@ -48,17 +45,8 @@ interface Props {
small?: boolean;
isOptional?: boolean;
passwordHelperText?: string;
}

export interface UserSSHKeyObject {
gravatarUrl: string;
username: string;
selected: boolean;
keys: string[];
onSSHKeyChange: (
e: React.ChangeEvent<HTMLInputElement>,
result: boolean
) => void;
setAuthorizedUsers?: (usernames: string[]) => void;
authorizedUsers?: string[];
}

type CombinedProps = Props & WithStyles<ClassNames>;
Expand All @@ -68,19 +56,18 @@ class AccessPanel extends React.Component<CombinedProps> {
const {
classes,
error,
sshKeyError,
label,
required,
placeholder,
users,
disabled,
disabledReason,
tooltipInteractive,
hideStrengthLabel,
className,
isOptional,
passwordHelperText,
requestKeys,
setAuthorizedUsers,
authorizedUsers,
} = this.props;

return (
Expand Down Expand Up @@ -112,14 +99,13 @@ class AccessPanel extends React.Component<CombinedProps> {
helperText={passwordHelperText}
/>
</React.Suspense>
{users && (
{setAuthorizedUsers !== undefined && authorizedUsers !== undefined && (
<>
<Divider spacingTop={44} spacingBottom={20} />
<UserSSHKeyPanel
users={users}
error={sshKeyError}
setAuthorizedUsers={setAuthorizedUsers}
authorizedUsers={authorizedUsers}
disabled={disabled}
onKeyAddSuccess={requestKeys || (() => null)}
/>
</>
)}
Expand Down
120 changes: 100 additions & 20 deletions packages/manager/src/components/AccessPanel/UserSSHKeyPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,116 @@
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

import { users } from 'src/__data__/userSSHKeyObjects';
import { QueryClient } from 'react-query';
import { profileFactory, sshKeyFactory } from 'src/factories';
import { accountUserFactory } from 'src/factories/accountUsers';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { rest, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';

import UserSSHKeyPanel from './UserSSHKeyPanel';

const props = {
onKeyAddSuccess: jest.fn(),
};

describe('UserSSHKeyPanel', () => {
describe('SSH panel', () => {
it('should render an empty state', () => {
describe('restricted user', () => {
it('should render an empty state', async () => {
// Mock a lot of API calls to simulate what a restricted user would get
server.use(
rest.get('*/profile', (req, res, ctx) => {
return res(ctx.json(profileFactory.build({ restricted: true })));
}),
rest.get('*/profile/sshkeys', (req, res, ctx) => {
return res(ctx.json(makeResourcePage([])));
}),
rest.get('*/account/users', (req, res, ctx) => {
return res(ctx.status(401), ctx.json(makeResourcePage([])));
})
);
const { queryByTestId } = renderWithTheme(
<UserSSHKeyPanel users={[]} {...props} />
<UserSSHKeyPanel authorizedUsers={[]} setAuthorizedUsers={jest.fn()} />
);
expect(queryByTestId('ssh-public-key')).toBeNull();
expect(queryByTestId('table-row-empty')).toBeInTheDocument();
await waitFor(() => {
expect(queryByTestId('table-row-empty')).toBeInTheDocument();
});
});
it('should render the restricted users ssh key if they have one', async () => {
// Mock a lot of API calls to simulate what a restricted user would get
server.use(
rest.get('*/profile', (req, res, ctx) => {
return res(ctx.json(profileFactory.build({ restricted: true })));
}),
rest.get('*/profile/sshkeys', (req, res, ctx) => {
const sshKeys = [
sshKeyFactory.build({ label: 'my-ssh-key' }),
sshKeyFactory.build({ label: 'my-other-ssh-key' }),
];
return res(ctx.json(makeResourcePage(sshKeys)));
}),
rest.get('*/account/users', (req, res, ctx) => {
return res(ctx.status(401), ctx.json(makeResourcePage([])));
})
);
const { getByText } = renderWithTheme(
<UserSSHKeyPanel authorizedUsers={[]} setAuthorizedUsers={jest.fn()} />,
{ queryClient: new QueryClient() }
);
await waitFor(() => {
expect(getByText('my-ssh-key', { exact: false })).toBeInTheDocument();
expect(
getByText('my-other-ssh-key', { exact: false })
).toBeInTheDocument();
});
});
});

it('should render a list of SSH keys', () => {
const { queryAllByTestId } = renderWithTheme(
<UserSSHKeyPanel users={users} {...props} />
describe('normal user', () => {
it('should render a row for each user', async () => {
server.use(
rest.get('*/profile', (req, res, ctx) => {
return res(ctx.json(profileFactory.build({ restricted: false })));
}),
rest.get('*/account/users', (req, res, ctx) => {
const users = [accountUserFactory.build({ username: 'test-user' })];
return res(ctx.json(makeResourcePage(users)));
})
);
const { getByText } = renderWithTheme(
<UserSSHKeyPanel authorizedUsers={[]} setAuthorizedUsers={jest.fn()} />,
{ queryClient: new QueryClient() }
);
expect(queryAllByTestId('ssh-public-key')).toHaveLength(users.length);
await waitFor(() => {
expect(getByText('test-user', { exact: false })).toBeInTheDocument();
});
});
it('should call the update handler when a user is checked', async () => {
server.use(
rest.get('*/profile', (req, res, ctx) => {
return res(ctx.json(profileFactory.build({ restricted: false })));
}),
rest.get('*/account/users', (req, res, ctx) => {
const users = [
accountUserFactory.build({
username: 'test-user',
ssh_keys: ['ssh-key'],
}),
];
return res(ctx.json(makeResourcePage(users)));
})
);

const props = {
authorizedUsers: [],
setAuthorizedUsers: jest.fn(),
};

it('should include a button to add a new key', () => {
const { queryByText } = renderWithTheme(
<UserSSHKeyPanel {...props} users={[]} />
const { getByText, getByRole } = renderWithTheme(
<UserSSHKeyPanel {...props} />,
{ queryClient: new QueryClient() }
);
expect(queryByText(/Add/i)).toBeInTheDocument();
await waitFor(() => {
expect(getByText('test-user')).toBeInTheDocument();
expect(getByText('ssh-key')).toBeInTheDocument();
});
userEvent.click(getByRole('checkbox'));
expect(props.setAuthorizedUsers).toBeCalledWith(['test-user']);
});
});
});
Loading