|
@@ -70,7 +70,7 @@ export const InputCell = ({
// followed this for icon in button best practices
// https://www.sarasoueidan.com/blog/accessible-icon-buttons/
-export const RemoveCell = ({ onClick, label }: { onClick: () => void; label: string }) => (
+const RemoveCell = ({ onClick, label }: { onClick: () => void; label: string }) => (
|
|
)
+
+type Column = {
+ header: string
+ cell: (item: T, index: number) => React.ReactNode
+}
+
+type MiniTableProps = {
+ ariaLabel: string
+ items: T[]
+ columns: Column[]
+ rowKey: (item: T, index: number) => string
+ onRemoveItem: (item: T) => void
+ removeLabel?: (item: T) => string
+ /**
+ * If empty state is not provided, the entire table will disappear when items
+ * is empty
+ */
+ emptyState?: { title: string; body: string }
+ className?: string
+}
+
+/** If `emptyState` is left out, `MiniTable` renders null when `items` is empty. */
+export function MiniTable({
+ ariaLabel,
+ items,
+ columns,
+ rowKey,
+ onRemoveItem,
+ removeLabel,
+ emptyState,
+ className,
+}: MiniTableProps) {
+ if (!emptyState && items.length === 0) return null
+
+ return (
+
+
+ {columns.map((column, index) => (
+ {column.header}
+ ))}
+ {/* For remove button */}
+
+
+
+ {items.length ? (
+ items.map((item, index) => (
+
+ {columns.map((column, colIndex) => (
+ | {column.cell(item, index)} |
+ ))}
+
+ onRemoveItem(item)}
+ label={removeLabel?.(item) || `Remove item ${index + 1}`}
+ />
+
+ ))
+ ) : emptyState ? (
+
+ ) : null}
+
+
+ )
+}
diff --git a/app/ui/styles/components/mini-table.css b/app/ui/styles/components/mini-table.css
index 5bbcc717bc..3bf03222fa 100644
--- a/app/ui/styles/components/mini-table.css
+++ b/app/ui/styles/components/mini-table.css
@@ -2,7 +2,7 @@
* 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
*/
@@ -28,6 +28,10 @@
content: ' ';
}
+ & tr td:last-child:before {
+ @apply hidden;
+ }
+
& tr:last-child td + td:before {
@apply bottom-[calc(0.5rem+2px)];
}
diff --git a/package-lock.json b/package-lock.json
index 74dd734ce7..73524f7779 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -57,7 +57,7 @@
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@mswjs/http-middleware": "^0.10.3",
"@oxide/openapi-gen-ts": "~0.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.53.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
@@ -1758,13 +1758,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
- "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
+ "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.52.0"
+ "playwright": "1.53.1"
},
"bin": {
"playwright": "cli.js"
@@ -11298,13 +11298,13 @@
}
},
"node_modules/playwright": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
- "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
+ "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.52.0"
+ "playwright-core": "1.53.1"
},
"bin": {
"playwright": "cli.js"
@@ -11317,9 +11317,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
- "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
+ "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/package.json b/package.json
index f86d570968..65dde430ea 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@mswjs/http-middleware": "^0.10.3",
"@oxide/openapi-gen-ts": "~0.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.53.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
diff --git a/test/e2e/firewall-rules.e2e.ts b/test/e2e/firewall-rules.e2e.ts
index 9ad4af8fd4..e62761ca4b 100644
--- a/test/e2e/firewall-rules.e2e.ts
+++ b/test/e2e/firewall-rules.e2e.ts
@@ -506,7 +506,7 @@ test('create from existing rule', async ({ page }) => {
await expect(portFilters).toBeHidden()
const targets = modal.getByRole('table', { name: 'Targets' })
- await expect(targets.getByRole('row', { name: 'Name: default, Type: vpc' })).toBeVisible()
+ await expect(targets.getByRole('row', { name: 'vpc default' })).toBeVisible()
// close the modal
await page.keyboard.press('Escape')
@@ -526,7 +526,7 @@ test('create from existing rule', async ({ page }) => {
await expect(modal.getByRole('checkbox', { name: 'UDP' })).not.toBeChecked()
await expect(modal.getByRole('checkbox', { name: 'ICMP' })).not.toBeChecked()
- await expect(targets.getByRole('row', { name: 'Name: default, Type: vpc' })).toBeVisible()
+ await expect(targets.getByRole('row', { name: 'vpc default' })).toBeVisible()
})
const rulePath = '/projects/mock-project/vpcs/mock-vpc/firewall-rules/allow-icmp/edit'
diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts
index 9d9357b381..33bba89cca 100644
--- a/test/e2e/instance-create.e2e.ts
+++ b/test/e2e/instance-create.e2e.ts
@@ -583,7 +583,7 @@ test('create instance with additional disks', async ({ page }) => {
const disksTable = page.getByRole('table', { name: 'Disks' })
await expect(disksTable.getByText('disk-6')).toBeHidden()
- await expectRowVisible(disksTable, { Name: 'new-disk-1', Type: 'create', Size: '5GiB' })
+ await expectRowVisible(disksTable, { Name: 'new-disk-1', Type: 'create', Size: '5 GiB' })
// now that name is taken too, so disk create disallows it
await page.getByRole('button', { name: 'Create new disk' }).click()
@@ -597,7 +597,7 @@ test('create instance with additional disks', async ({ page }) => {
await selectOption(page, 'Disk name', 'disk-3')
await page.getByRole('button', { name: 'Attach disk' }).click()
- await expectRowVisible(disksTable, { Name: 'disk-3', Type: 'attach', Size: '—' })
+ await expectRowVisible(disksTable, { Name: 'disk-3', Type: 'attach', Size: '6 GiB' })
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
|