From a71545bdcfc00df015ea306f706b0c2936d6b7c4 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Mon, 2 Dec 2024 18:43:15 +0000 Subject: [PATCH 1/9] Add view/edit SSH key page --- app/forms/ssh-key-edit.tsx | 75 +++++++++++++++++++ app/hooks/use-params.ts | 2 + app/pages/settings/SSHKeysPage.tsx | 21 ++++-- app/routes.tsx | 10 ++- .../__snapshots__/path-builder.spec.ts.snap | 10 +++ app/util/path-builder.spec.ts | 2 + app/util/path-builder.ts | 2 + mock-api/sshKeys.ts | 6 +- test/e2e/ssh-keys.e2e.ts | 25 ++++++- 9 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 app/forms/ssh-key-edit.tsx diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx new file mode 100644 index 0000000000..5fa6ab3656 --- /dev/null +++ b/app/forms/ssh-key-edit.tsx @@ -0,0 +1,75 @@ +import { useForm } from 'react-hook-form' +import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' + +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { Key16Icon } from '@oxide/design-system/icons/react' + +import { DescriptionField } from '~/components/form/fields/DescriptionField' +import { NameField } from '~/components/form/fields/NameField' +import { TextField } from '~/components/form/fields/TextField' +import { SideModalForm } from '~/components/form/SideModalForm' +import { getSshKeySelector, useSshKeySelector } from '~/hooks/use-params' +import { DateTime } from '~/ui/lib/DateTime' +import { PropertiesTable } from '~/ui/lib/PropertiesTable' +import { ResourceLabel } from '~/ui/lib/SideModal' +import { Truncate } from '~/ui/lib/Truncate' +import { pb } from '~/util/path-builder' + +EditSSHKeySideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { + const { sshKey } = getSshKeySelector(params) + await apiQueryClient.prefetchQuery('currentUserSshKeyView', { path: { sshKey } }) + return null +} + +export function EditSSHKeySideModalForm() { + const navigate = useNavigate() + const { sshKey } = useSshKeySelector() + + const onDismiss = () => navigate(pb.sshKeys()) + + const { data } = usePrefetchedApiQuery('currentUserSshKeyView', { + path: { sshKey }, + }) + + const form = useForm({ defaultValues: data }) + + return ( + + {data.name} + + } + // TODO: pass actual error when this form is hooked up + loading={false} + submitError={null} + > + + + + + + + + + + + + + + + + ) +} diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 348e48d3b1..7bee58f446 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -42,6 +42,7 @@ export const getVpcRouterRouteSelector = requireParams('project', 'vpc', 'router export const getVpcSubnetSelector = requireParams('project', 'vpc', 'subnet') export const getSiloSelector = requireParams('silo') export const getSiloImageSelector = requireParams('image') +export const getSshKeySelector = requireParams('sshKey') export const getIdpSelector = requireParams('silo', 'provider') export const getProjectImageSelector = requireParams('project', 'image') export const getProjectSnapshotSelector = requireParams('project', 'snapshot') @@ -77,6 +78,7 @@ function useSelectedParams(getSelector: (params: AllParams) => T) { export const useFloatingIpSelector = () => useSelectedParams(getFloatingIpSelector) export const useProjectSelector = () => useSelectedParams(getProjectSelector) export const useProjectImageSelector = () => useSelectedParams(getProjectImageSelector) +export const useSshKeySelector = () => useSelectedParams(getSshKeySelector) export const useProjectSnapshotSelector = () => useSelectedParams(getProjectSnapshotSelector) export const useInstanceSelector = () => useSelectedParams(getInstanceSelector) diff --git a/app/pages/settings/SSHKeysPage.tsx b/app/pages/settings/SSHKeysPage.tsx index 9e2bd58d5a..53d40fdde1 100644 --- a/app/pages/settings/SSHKeysPage.tsx +++ b/app/pages/settings/SSHKeysPage.tsx @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ import { createColumnHelper } from '@tanstack/react-table' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { Link, Outlet, useNavigate } from 'react-router-dom' import { @@ -22,7 +22,8 @@ import { DocsPopover } from '~/components/DocsPopover' import { HL } from '~/components/HL' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' -import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' +import { makeLinkCell } from '~/table/cells/LinkCell' +import { getActionsCol, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { buttonStyle } from '~/ui/lib/Button' @@ -39,11 +40,6 @@ export async function loader() { } const colHelper = createColumnHelper() -const staticCols = [ - colHelper.accessor('name', {}), - colHelper.accessor('description', Columns.description), - colHelper.accessor('timeModified', Columns.timeModified), -] Component.displayName = 'SSHKeysPage' export function Component() { @@ -71,6 +67,16 @@ export function Component() { [deleteSshKey] ) + const columns = useMemo(() => { + return [ + colHelper.accessor('name', { + cell: makeLinkCell((sshKey) => pb.sshKeyEdit({ sshKey: sshKey })), + }), + colHelper.accessor('description', Columns.description), + getActionsCol(makeActions), + ] + }, [makeActions]) + const emptyState = ( } @@ -80,7 +86,6 @@ export function Component() { onClick={() => navigate(pb.sshKeysNew())} /> ) - const columns = useColsWithActions(staticCols, makeActions) const { table } = useQueryTable({ query: sshKeyList(), columns, emptyState }) return ( diff --git a/app/routes.tsx b/app/routes.tsx index ccc13b8543..57d3f52a70 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -28,6 +28,7 @@ import { EditProjectSideModalForm } from './forms/project-edit' import { CreateSiloSideModalForm } from './forms/silo-create' import * as SnapshotCreate from './forms/snapshot-create' import * as SSHKeyCreate from './forms/ssh-key-create' +import { EditSSHKeySideModalForm } from './forms/ssh-key-edit' import { CreateSubnetForm } from './forms/subnet-create' import { EditSubnetForm } from './forms/subnet-edit' import { CreateVpcSideModalForm } from './forms/vpc-create' @@ -118,7 +119,14 @@ export const routes = createRoutesFromElements( } /> } handle={{ crumb: 'Profile' }} /> - + + } + handle={titleCrumb('Edit SSH Key')} + /> + diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index a4c74b445e..debb608d86 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -551,6 +551,16 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/snapshots", }, ], + "sshKeyEdit (/settings/ssh-keys/ss/edit)": [ + { + "label": "Settings", + "path": "/settings/profile", + }, + { + "label": "SSH Keys", + "path": "/settings/ssh-keys", + }, + ], "sshKeys (/settings/ssh-keys)": [ { "label": "Settings", diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index ff32e3e277..291447a1ba 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -25,6 +25,7 @@ const params = { provider: 'pr', sledId: '5c56b522-c9b8-49e4-9f9a-8d52a89ec3e0', image: 'im', + sshKey: 'ss', snapshot: 'sn', pool: 'pl', rule: 'fr', @@ -82,6 +83,7 @@ test('path builder', () => { "snapshotImagesNew": "/projects/p/snapshots/sn/images-new", "snapshots": "/projects/p/snapshots", "snapshotsNew": "/projects/p/snapshots-new", + "sshKeyEdit": "/settings/ssh-keys/ss/edit", "sshKeys": "/settings/ssh-keys", "sshKeysNew": "/settings/ssh-keys-new", "systemUtilization": "/system/utilization", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 2fb132d64a..0c43ca4f22 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -25,6 +25,7 @@ type FirewallRule = Required type VpcRouter = Required type VpcRouterRoute = Required type VpcSubnet = Required +type SshKey = Required // these are used as the basis for many routes but are not themselves routes we // ever want to link to. so we use this to build the routes but pb.project() is @@ -129,6 +130,7 @@ export const pb = { profile: () => '/settings/profile', sshKeys: () => '/settings/ssh-keys', sshKeysNew: () => '/settings/ssh-keys-new', + sshKeyEdit: (params: SshKey) => `/settings/ssh-keys/${params.sshKey}/edit`, deviceSuccess: () => '/device/success', } diff --git a/mock-api/sshKeys.ts b/mock-api/sshKeys.ts index f5c72fef4c..7cbf5771a4 100644 --- a/mock-api/sshKeys.ts +++ b/mock-api/sshKeys.ts @@ -17,7 +17,8 @@ export const sshKeys: Json[] = [ description: 'For use on personal projects', time_created: new Date().toISOString(), time_modified: new Date().toISOString(), - public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', + public_key: + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU3w4FaSj/tEZYaoBtzijAFZanW9MakaPhSERtdC75opT6F/bs4ZXE8sjWgqDM1azoZbUKa42b4RWPPtCgqGQkbyYDZTzdssrml3/T1Avcy5GKlfTjACRHSI6PhC6r6bM1jxPUUstH7fBbw+DTHywUpdkvz7SHxTEOyZuP2sn38V9vBakYVsLFOu7C1W0+Jm4TYCRJlcsuC5LHVMVc4WbWzBcAZZlAznWx0XajMxmkyCB5tsyhTpykabfHbih4F3bwHYKXO613JZ6DurGcPz6CPkAVS5BWG6GrdBCkd+YK8Lw8k1oAAZLYIKQZbMnPJSNxirJ8+vr+iyIwP1DjBMnJ hannah@m1-macbook-pro.local', silo_user_id: user1.id, }, { @@ -26,7 +27,8 @@ export const sshKeys: Json[] = [ description: '', time_created: new Date().toISOString(), time_modified: new Date().toISOString(), - public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd', + public_key: + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVav/u9wm2ALv4ks18twWQB0LHlJ1Q1y7HTi91SUuv4H95EEwqj6tVDSOtHQi08PG7xp6/8gaMVC9rs1jKl7o0cy32kuWp/rXtryn3d1bEaY9wOGwR6iokx0zjocHILhrjHpAmWnXP8oWvzx8TWOg3VPhBkZsyNdqzcdxYP2UsqccaNyz5kcuNhOYbGjIskNPAk1drsHnyKvqoEVix8UzVkLHC6vVbcVjQGTaeUif29xvUN3W5QMGb/E1L66RPN3ovaDyDylgA8az8q56vrn4jSY5Mx3ANQEvjxl//Hnq31dpoDFiEvHyB4bbq8bSpypa2TyvheobmLnsnIaXEMHFT hannah@mac-mini.local', silo_user_id: user1.id, }, ] diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index fd6768bfb2..8ce387ac63 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { test } from '@playwright/test' +import { expect, test } from '@playwright/test' import { clickRowAction, expectNotVisible, expectRowVisible, expectVisible } from './utils' @@ -19,6 +19,29 @@ test('SSH keys', async ({ page }) => { 'role=cell[name="mac-mini"]', ]) + // click name to open side modal + await page.getByRole('link', { name: 'm1-macbook-pro' }).click() + + // verify side modal content + const modal = page.getByRole('dialog') + await expect(modal).toBeVisible() + await expect(modal.getByText('Edit SSH key')).toBeVisible() + await expect(modal.getByText('m1-macbook-pro')).toBeVisible() + + const propertiesTable = modal.locator('.properties-table') + await expect(propertiesTable.getByText('ID')).toBeVisible() + await expect(propertiesTable.getByText('Created')).toBeVisible() + await expect(propertiesTable.getByText('Updated')).toBeVisible() + + // verify form fields are present and disabled + await expect(modal.getByRole('textbox', { name: 'Name' })).toBeDisabled() + await expect(modal.getByRole('textbox', { name: 'Description' })).toBeDisabled() + await expect(modal.getByRole('textbox', { name: 'Public key' })).toBeDisabled() + + // close modal + await modal.getByRole('button', { name: 'Close' }).click() + await expect(modal).toBeHidden() + // delete the two ssh keys await clickRowAction(page, 'm1-macbook-pro', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() From c7d3c561632704c9f3bba162730471b9bc451a21 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 3 Dec 2024 10:31:31 +0000 Subject: [PATCH 2/9] Tweak `expectVisible` to take container --- test/e2e/ssh-keys.e2e.ts | 10 +++++----- test/e2e/utils.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index 8ce387ac63..8eaf3c7f94 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -25,13 +25,13 @@ test('SSH keys', async ({ page }) => { // verify side modal content const modal = page.getByRole('dialog') await expect(modal).toBeVisible() - await expect(modal.getByText('Edit SSH key')).toBeVisible() - await expect(modal.getByText('m1-macbook-pro')).toBeVisible() + await expectVisible(page, [ + 'role=heading[name="m1-macbook-pro"]', + 'role=heading[name="Edit SSH key"]', + ]) const propertiesTable = modal.locator('.properties-table') - await expect(propertiesTable.getByText('ID')).toBeVisible() - await expect(propertiesTable.getByText('Created')).toBeVisible() - await expect(propertiesTable.getByText('Updated')).toBeVisible() + await expectVisible(propertiesTable, ['text="ID"', 'text="Created"', 'text="Updated"']) // verify form fields are present and disabled await expect(modal.getByRole('textbox', { name: 'Name' })).toBeDisabled() diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index 5d477cb80a..95fd5f890f 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -36,18 +36,18 @@ export async function map( type Selector = string | Locator -const toLocator = (page: Page, selector: Selector): Locator => - typeof selector === 'string' ? page.locator(selector) : selector - -export async function expectVisible(page: Page, selectors: Selector[]) { +// note if `Locator` is used instead of string, the container is ignored +export async function expectVisible(container: Page | Locator, selectors: Selector[]) { for (const selector of selectors) { - await expect(toLocator(page, selector)).toBeVisible() + const locator = typeof selector === 'string' ? container.locator(selector) : selector + await expect(locator).toBeVisible() } } -export async function expectNotVisible(page: Page, selectors: Selector[]) { +export async function expectNotVisible(container: Page | Locator, selectors: Selector[]) { for (const selector of selectors) { - await expect(toLocator(page, selector)).toBeHidden() + const locator = typeof selector === 'string' ? container.locator(selector) : selector + await expect(locator).toBeHidden() } } From cfd3d29a273924d45a0862aa62a650750de6fab0 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 3 Dec 2024 11:02:13 +0000 Subject: [PATCH 3/9] Add copy button --- app/forms/ssh-key-edit.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx index 5fa6ab3656..6e7b16155d 100644 --- a/app/forms/ssh-key-edit.tsx +++ b/app/forms/ssh-key-edit.tsx @@ -1,3 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' @@ -9,6 +16,7 @@ import { NameField } from '~/components/form/fields/NameField' import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' import { getSshKeySelector, useSshKeySelector } from '~/hooks/use-params' +import { CopyToClipboard } from '~/ui/lib/CopyToClipboard' import { DateTime } from '~/ui/lib/DateTime' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel } from '~/ui/lib/SideModal' @@ -61,15 +69,18 @@ export function EditSSHKeySideModalForm() { - +
+ + +
) } From ba52cca9c7b2ab778b7868f5899e7d243021cd72 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 3 Dec 2024 11:03:21 +0000 Subject: [PATCH 4/9] Add copy in actions menu --- app/pages/settings/SSHKeysPage.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/pages/settings/SSHKeysPage.tsx b/app/pages/settings/SSHKeysPage.tsx index 53d40fdde1..f52b5499c6 100644 --- a/app/pages/settings/SSHKeysPage.tsx +++ b/app/pages/settings/SSHKeysPage.tsx @@ -56,6 +56,12 @@ export function Component() { const makeActions = useCallback( (sshKey: SshKey): MenuAction[] => [ + { + label: 'Copy public key', + onActivate() { + window.navigator.clipboard.writeText(sshKey.publicKey) + }, + }, { label: 'Delete', onActivate: confirmDelete({ From 2625772d12aa1793a46a70aed5c31ee8f7d42ec3 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 3 Dec 2024 14:15:04 +0000 Subject: [PATCH 5/9] Revert "Tweak `expectVisible` to take container" This reverts commit c7d3c561632704c9f3bba162730471b9bc451a21. --- test/e2e/ssh-keys.e2e.ts | 10 +++++----- test/e2e/utils.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index 8eaf3c7f94..8ce387ac63 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -25,13 +25,13 @@ test('SSH keys', async ({ page }) => { // verify side modal content const modal = page.getByRole('dialog') await expect(modal).toBeVisible() - await expectVisible(page, [ - 'role=heading[name="m1-macbook-pro"]', - 'role=heading[name="Edit SSH key"]', - ]) + await expect(modal.getByText('Edit SSH key')).toBeVisible() + await expect(modal.getByText('m1-macbook-pro')).toBeVisible() const propertiesTable = modal.locator('.properties-table') - await expectVisible(propertiesTable, ['text="ID"', 'text="Created"', 'text="Updated"']) + await expect(propertiesTable.getByText('ID')).toBeVisible() + await expect(propertiesTable.getByText('Created')).toBeVisible() + await expect(propertiesTable.getByText('Updated')).toBeVisible() // verify form fields are present and disabled await expect(modal.getByRole('textbox', { name: 'Name' })).toBeDisabled() diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index 95fd5f890f..5d477cb80a 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -36,18 +36,18 @@ export async function map( type Selector = string | Locator -// note if `Locator` is used instead of string, the container is ignored -export async function expectVisible(container: Page | Locator, selectors: Selector[]) { +const toLocator = (page: Page, selector: Selector): Locator => + typeof selector === 'string' ? page.locator(selector) : selector + +export async function expectVisible(page: Page, selectors: Selector[]) { for (const selector of selectors) { - const locator = typeof selector === 'string' ? container.locator(selector) : selector - await expect(locator).toBeVisible() + await expect(toLocator(page, selector)).toBeVisible() } } -export async function expectNotVisible(container: Page | Locator, selectors: Selector[]) { +export async function expectNotVisible(page: Page, selectors: Selector[]) { for (const selector of selectors) { - const locator = typeof selector === 'string' ? container.locator(selector) : selector - await expect(locator).toBeHidden() + await expect(toLocator(page, selector)).toBeHidden() } } From 146ecfb6af0816967605651b5adc316f895102f4 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 3 Dec 2024 14:16:42 +0000 Subject: [PATCH 6/9] Update ssh-keys.e2e.ts --- test/e2e/ssh-keys.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index 8ce387ac63..c537dafbdb 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -26,7 +26,7 @@ test('SSH keys', async ({ page }) => { const modal = page.getByRole('dialog') await expect(modal).toBeVisible() await expect(modal.getByText('Edit SSH key')).toBeVisible() - await expect(modal.getByText('m1-macbook-pro')).toBeVisible() + await expect(modal.getByRole('heading', { name: 'm1-macbook-pro' })).toBeVisible() const propertiesTable = modal.locator('.properties-table') await expect(propertiesTable.getByText('ID')).toBeVisible() From b24533ff9ef838c53beea9eb5aec422df8ea31df Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 6 Dec 2024 17:09:54 -0600 Subject: [PATCH 7/9] rename form to "View SSH key" --- app/forms/ssh-key-edit.tsx | 5 ++--- app/routes.tsx | 2 +- test/e2e/ssh-keys.e2e.ts | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx index 6e7b16155d..609ec225ce 100644 --- a/app/forms/ssh-key-edit.tsx +++ b/app/forms/ssh-key-edit.tsx @@ -33,8 +33,6 @@ export function EditSSHKeySideModalForm() { const navigate = useNavigate() const { sshKey } = useSshKeySelector() - const onDismiss = () => navigate(pb.sshKeys()) - const { data } = usePrefetchedApiQuery('currentUserSshKeyView', { path: { sshKey }, }) @@ -46,7 +44,8 @@ export function EditSSHKeySideModalForm() { form={form} formType="edit" resourceName="SSH key" - onDismiss={onDismiss} + title="View SSH key" + onDismiss={() => navigate(pb.sshKeys())} subtitle={ {data.name} diff --git a/app/routes.tsx b/app/routes.tsx index 57d3f52a70..19e8e671a1 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -124,7 +124,7 @@ export const routes = createRoutesFromElements( path=":sshKey/edit" loader={EditSSHKeySideModalForm.loader} element={} - handle={titleCrumb('Edit SSH Key')} + handle={titleCrumb('View SSH Key')} />
diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index c537dafbdb..6670b43b26 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -23,9 +23,8 @@ test('SSH keys', async ({ page }) => { await page.getByRole('link', { name: 'm1-macbook-pro' }).click() // verify side modal content - const modal = page.getByRole('dialog') + const modal = page.getByRole('dialog', { name: 'View SSH key' }) await expect(modal).toBeVisible() - await expect(modal.getByText('Edit SSH key')).toBeVisible() await expect(modal.getByRole('heading', { name: 'm1-macbook-pro' })).toBeVisible() const propertiesTable = modal.locator('.properties-table') From 137dcd281f42cb299f11a4d63318685a01c1a817 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 6 Dec 2024 17:19:29 -0600 Subject: [PATCH 8/9] don't use stringy locators and expectVisible --- test/e2e/ssh-keys.e2e.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index 6670b43b26..6bd7c357f1 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -7,17 +7,15 @@ */ import { expect, test } from '@playwright/test' -import { clickRowAction, expectNotVisible, expectRowVisible, expectVisible } from './utils' +import { clickRowAction, expectRowVisible } from './utils' test('SSH keys', async ({ page }) => { await page.goto('/settings/ssh-keys') // see table with the ssh key - await expectVisible(page, [ - 'role=heading[name*="SSH Keys"]', - 'role=cell[name="m1-macbook-pro"]', - 'role=cell[name="mac-mini"]', - ]) + await expect(page.getByRole('heading', { name: 'SSH Keys' })).toBeVisible() + await expect(page.getByRole('cell', { name: 'm1-macbook-pro' })).toBeVisible() + await expect(page.getByRole('cell', { name: 'mac-mini' })).toBeVisible() // click name to open side modal await page.getByRole('link', { name: 'm1-macbook-pro' }).click() @@ -39,39 +37,39 @@ test('SSH keys', async ({ page }) => { // close modal await modal.getByRole('button', { name: 'Close' }).click() - await expect(modal).toBeHidden() + await expect(modal).not.toBeVisible() // delete the two ssh keys await clickRowAction(page, 'm1-macbook-pro', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() - await expectNotVisible(page, ['role=cell[name="m1-macbook-pro"]']) + await expect(page.getByRole('cell', { name: 'm1-macbook-pro' })).not.toBeVisible() await clickRowAction(page, 'mac-mini', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() // should show empty state - await expectVisible(page, ['text="No SSH keys"']) + await expect(page.getByText('No SSH keys')).toBeVisible() // there are two of these, but it doesn't matter which one we click - await page.click('role=button[name="Add SSH key"]') + await page.getByRole('button', { name: 'Add SSH key' }).click() // fill out form and submit - await page.fill('role=textbox[name="Name"]', 'my-key') - await page.fill('role=textbox[name="Description"]', 'definitely a key') - await page.fill('role=textbox[name="Public key"]', 'key contents') + await page.getByRole('textbox', { name: 'Name' }).fill('my-key') + await page.getByRole('textbox', { name: 'Description' }).fill('definitely a key') + await page.getByRole('textbox', { name: 'Public key' }).fill('key contents') await page.getByRole('dialog').getByRole('button', { name: 'Add SSH key' }).click() // it's there in the table - await expectNotVisible(page, ['text="No SSH keys"']) + await expect(page.getByText('No SSH keys')).not.toBeVisible() const table = page.getByRole('table') await expectRowVisible(table, { name: 'my-key', description: 'definitely a key' }) // now delete it - await page.click('role=button[name="Row actions"]') - await page.click('role=menuitem[name="Delete"]') + await page.getByRole('button', { name: 'Row actions' }).click() + await page.getByRole('menuitem', { name: 'Delete' }).click() await page.getByRole('button', { name: 'Confirm' }).click() - await expectNotVisible(page, ['role=cell[name="my-key"]']) - await expectVisible(page, ['text="No SSH keys"']) + await expect(page.getByRole('cell', { name: 'my-key' })).not.toBeVisible() + await expect(page.getByText('No SSH keys')).toBeVisible() }) From 61d6f06196d6babf28c08ae8701e3a56c65e9ac0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:20:37 +0000 Subject: [PATCH 9/9] Bot commit: format with prettier --- test/e2e/ssh-keys.e2e.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/ssh-keys.e2e.ts b/test/e2e/ssh-keys.e2e.ts index 6bd7c357f1..0f45d7da65 100644 --- a/test/e2e/ssh-keys.e2e.ts +++ b/test/e2e/ssh-keys.e2e.ts @@ -37,13 +37,13 @@ test('SSH keys', async ({ page }) => { // close modal await modal.getByRole('button', { name: 'Close' }).click() - await expect(modal).not.toBeVisible() + await expect(modal).toBeHidden() // delete the two ssh keys await clickRowAction(page, 'm1-macbook-pro', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() - await expect(page.getByRole('cell', { name: 'm1-macbook-pro' })).not.toBeVisible() + await expect(page.getByRole('cell', { name: 'm1-macbook-pro' })).toBeHidden() await clickRowAction(page, 'mac-mini', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() @@ -61,7 +61,7 @@ test('SSH keys', async ({ page }) => { await page.getByRole('dialog').getByRole('button', { name: 'Add SSH key' }).click() // it's there in the table - await expect(page.getByText('No SSH keys')).not.toBeVisible() + await expect(page.getByText('No SSH keys')).toBeHidden() const table = page.getByRole('table') await expectRowVisible(table, { name: 'my-key', description: 'definitely a key' }) @@ -70,6 +70,6 @@ test('SSH keys', async ({ page }) => { await page.getByRole('menuitem', { name: 'Delete' }).click() await page.getByRole('button', { name: 'Confirm' }).click() - await expect(page.getByRole('cell', { name: 'my-key' })).not.toBeVisible() + await expect(page.getByRole('cell', { name: 'my-key' })).toBeHidden() await expect(page.getByText('No SSH keys')).toBeVisible() })