Skip to content

Commit 12c1e5c

Browse files
authored
Display external IPs on primary network interface (#1070)
* stub out basics * display external IPs on primary network interface * assert rows with an object instead of an array * point to omicron main now that the ResultsPage change is merged
1 parent 9868547 commit 12c1e5c

File tree

11 files changed

+116
-60
lines changed

11 files changed

+116
-60
lines changed

.github/workflows/lintBuildTest.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ jobs:
4646
- name: Install Playwright
4747
run: npx playwright install --with-deps
4848
- name: Run Playwright tests
49-
run: yarn playwright test
49+
run: yarn playwright test --workers=3

OMICRON_VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5e835f19680655a89ff1dca2a6e18f1f269ac021
1+
6d8d6a4580db44a885f7a213aa39ada76d8af2d6

app/pages/__tests__/instance/networking.e2e.ts

+5-17
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ test('Instance networking tab', async ({ page }) => {
99

1010
// Instance networking tab
1111
await page.click('role=tab[name="Networking"]')
12-
await expectRowVisible(page, 'my-nic', [
13-
'my-nic',
14-
'a network interface',
15-
'172.30.0.10',
16-
'mock-vpc',
17-
'mock-subnet',
18-
'primary',
19-
])
12+
13+
const table = page.locator('table')
14+
await expectRowVisible(table, { name: 'my-nic', primary: 'primary' })
2015

2116
// check VPC link in table points to the right page
2217
await expect(page.locator('role=cell >> role=link[name="mock-vpc"]')).toHaveAttribute(
@@ -54,15 +49,8 @@ test('Instance networking tab', async ({ page }) => {
5449
.locator('role=button[name="Row actions"]')
5550
.click()
5651
await page.click('role=menuitem[name="Make primary"]')
57-
await expectRowVisible(page, 'my-nic', [
58-
'my-nic',
59-
'a network interface',
60-
'172.30.0.10',
61-
'mock-vpc',
62-
'mock-subnet',
63-
'',
64-
])
65-
await expectRowVisible(page, 'nic-2', ['nic-2', null, null, null, null, 'primary'])
52+
await expectRowVisible(table, { name: 'my-nic', primary: '' })
53+
await expectRowVisible(table, { name: 'nic-2', primary: 'primary' })
6654

6755
// Make an edit to the network interface
6856
await page

app/pages/__tests__/org-access.e2e.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e'
55
test('Click through org access page', async ({ page }) => {
66
await page.goto('/orgs/maze-war')
77

8-
// page is there, we see AL but not FDR
8+
const table = page.locator('role=table')
9+
10+
// page is there, we see user 1 but not 2
911
await page.click('role=link[name*="Access & IAM"]')
1012
await expectVisible(page, ['role=heading[name*="Access & IAM"]'])
11-
await expectRowVisible(page, 'user-1', ['user-1', 'Hannah Arendt', 'admin'])
13+
await expectRowVisible(table, { ID: 'user-1', Name: 'Hannah Arendt', Role: 'admin' })
1214
await expectNotVisible(page, ['role=cell[name="user-2"]'])
1315

14-
// Add FDR as collab
16+
// Add user 2 as collab
1517
await page.click('role=button[name="Add user to organization"]')
1618
await expectVisible(page, ['role=heading[name*="Add user to organization"]'])
1719

@@ -32,10 +34,10 @@ test('Click through org access page', async ({ page }) => {
3234
await page.click('role=option[name="Collaborator"]')
3335
await page.click('role=button[name="Add user"]')
3436

35-
// FDR shows up in the table
36-
await expectRowVisible(page, 'user-2', ['user-2', 'Hans Jonas', 'collaborator'])
37+
// User 2 shows up in the table
38+
await expectRowVisible(table, { ID: 'user-2', Name: 'Hans Jonas', Role: 'collaborator' })
3739

38-
// now change FDR's role from collab to viewer
40+
// now change user 2's role from collab to viewer
3941
await page
4042
.locator('role=row', { hasText: 'user-2' })
4143
.locator('role=button[name="Row actions"]')
@@ -49,9 +51,9 @@ test('Click through org access page', async ({ page }) => {
4951
await page.click('role=option[name="Viewer"]')
5052
await page.click('role=button[name="Update role"]')
5153

52-
await expectRowVisible(page, 'user-2', ['user-2', 'Hans Jonas', 'viewer'])
54+
await expectRowVisible(table, { ID: 'user-2', Role: 'viewer' })
5355

54-
// now delete FDR
56+
// now delete user 2
5557
await page
5658
.locator('role=row', { hasText: 'user-2' })
5759
.locator('role=button[name="Row actions"]')

app/pages/__tests__/project-access.e2e.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e'
44

55
test('Click through project access page', async ({ page }) => {
66
await page.goto('/orgs/maze-war/projects/mock-project')
7-
8-
// page is there, we see AL but not FDR
97
await page.click('role=link[name*="Access & IAM"]')
8+
9+
// page is there, we see user 1 but not 2
1010
await expectVisible(page, ['role=heading[name*="Access & IAM"]'])
11-
await expectRowVisible(page, 'user-1', ['user-1', 'Hannah Arendt', 'admin'])
11+
const table = page.locator('table')
12+
await expectRowVisible(table, { ID: 'user-1', Name: 'Hannah Arendt', Role: 'admin' })
1213
await expectNotVisible(page, ['role=cell[name="user-2"]'])
1314

14-
// Add FDR as collab
15+
// Add user 2 as collab
1516
await page.click('role=button[name="Add user to project"]')
1617
await expectVisible(page, ['role=heading[name*="Add user to project"]'])
1718

@@ -32,10 +33,10 @@ test('Click through project access page', async ({ page }) => {
3233
await page.click('role=option[name="Collaborator"]')
3334
await page.click('role=button[name="Add user"]')
3435

35-
// FDR shows up in the table
36-
await expectRowVisible(page, 'user-2', ['user-2', 'Hans Jonas', 'collaborator'])
36+
// User 2 shows up in the table
37+
await expectRowVisible(table, { ID: 'user-2', Name: 'Hans Jonas', Role: 'collaborator' })
3738

38-
// now change FDR's role from collab to viewer
39+
// now change user 2 role from collab to viewer
3940
await page
4041
.locator('role=row', { hasText: 'user-2' })
4142
.locator('role=button[name="Row actions"]')
@@ -49,9 +50,9 @@ test('Click through project access page', async ({ page }) => {
4950
await page.click('role=option[name="Viewer"]')
5051
await page.click('role=button[name="Update role"]')
5152

52-
await expectRowVisible(page, 'user-2', ['user-2', 'Hans Jonas', 'viewer'])
53+
await expectRowVisible(table, { ID: 'user-2', Role: 'viewer' })
5354

54-
// now delete FDR
55+
// now delete user 2
5556
await page
5657
.locator('role=row', { hasText: 'user-2' })
5758
.locator('role=button[name="Row actions"]')

app/pages/__tests__/ssh-keys.e2e.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ test('SSH keys', async ({ page }) => {
1818

1919
// it's there in the table
2020
await expectNotVisible(page, ['text="No SSH keys"'])
21-
await expectRowVisible(page, 'my-key', ['my-key', 'definitely a key'])
21+
const table = page.locator('role=table')
22+
await expectRowVisible(table, { Name: 'my-key', Description: 'definitely a key' })
2223

2324
// now delete it
2425
await page.click('role=button[name="Row actions"]')

app/pages/project/instances/instance/tabs/NetworkingTab.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ const SubnetNameFromId = ({ value }: { value: string }) => (
3939
</span>
4040
)
4141

42+
function ExternalIpsFromInstanceName({ value: primary }: { value: boolean }) {
43+
const instanceParams = useParams('orgName', 'projectName', 'instanceName')
44+
const { data } = useApiQuery('instanceExternalIpList', instanceParams)
45+
const ips = data?.items.map((eip) => eip.ip).join(', ')
46+
return <span className="text-default">{primary ? ips : <>&mdash;</>}</span>
47+
}
48+
4249
export function NetworkingTab() {
4350
const instanceParams = useParams('orgName', 'projectName', 'instanceName')
4451
const queryClient = useApiQueryClient()
@@ -119,6 +126,12 @@ export function NetworkingTab() {
119126
<Column accessor="description" />
120127
{/* TODO: mark v4 or v6 explicitly? */}
121128
<Column accessor="ip" />
129+
<Column
130+
header="External IP"
131+
// we use primary to decide whether to show the IP in that row
132+
accessor="primary"
133+
cell={ExternalIpsFromInstanceName}
134+
/>
122135
<Column header="vpc" accessor="vpcId" cell={VpcNameFromId} />
123136
<Column header="subnet" accessor="subnetId" cell={SubnetNameFromId} />
124137
<Column

app/util/e2e.ts

+42-21
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import type { Locator, Page } from '@playwright/test'
22
import { expect } from '@playwright/test'
33

4-
export async function forEach(loc: Locator, fn: (loc0: Locator) => void) {
4+
export async function forEach(loc: Locator, fn: (loc0: Locator, i: number) => void) {
55
const count = await loc.count()
66
for (let i = 0; i < count; i++) {
7-
await fn(loc.nth(i))
7+
await fn(loc.nth(i), i)
88
}
99
}
1010

11+
export async function map<T>(
12+
loc: Locator,
13+
fn: (loc0: Locator, i: number) => Promise<T>
14+
): Promise<T[]> {
15+
const result: T[] = []
16+
await forEach(loc, async (loc0, i) => {
17+
result.push(await fn(loc0, i))
18+
})
19+
return result
20+
}
21+
1122
export async function expectVisible(page: Page, selectors: string[]) {
1223
for (const selector of selectors) {
1324
await expect(page.locator(selector)).toBeVisible()
@@ -21,26 +32,36 @@ export async function expectNotVisible(page: Page, selectors: string[]) {
2132
}
2233

2334
/**
24-
* Assert about the values of a row, identified by `rowSelectorText`. It doesn't
25-
* need to be the entire row; the test will pass as long as the identified row
26-
* exists and the first N cells match the N values in `cellTexts`. Pass `''` for
27-
* a checkbox cell.
28-
*
29-
* @param rowSelectorText Text that should uniquely identify the row, like an ID
30-
* @param cellTexts Text to match in each cell of that row
35+
* Assert that a row matching `expectedRow` is present in `table`. The match
36+
* uses `objectContaining`, so `expectedRow` does not need to contain every
37+
* cell. Works by converting `table` to a list of objects where the keys are
38+
* header cell text and the values are row cell text.
3139
*/
3240
export async function expectRowVisible(
33-
page: Page,
34-
rowSelectorText: string,
35-
cellTexts: Array<string | null>
41+
table: Locator,
42+
expectedRow: Record<string, string>
3643
) {
37-
const row = page.locator(`tr:has-text("${rowSelectorText}")`)
38-
await expect(row).toBeVisible()
39-
for (let i = 0; i < cellTexts.length; i++) {
40-
const text = cellTexts[i]
41-
if (text === null) {
42-
continue
43-
}
44-
await expect(row.locator(`role=cell >> nth=${i}`)).toHaveText(text)
45-
}
44+
// wait for header and rows to avoid flake town
45+
const headerLoc = table.locator('thead >> role=cell')
46+
await headerLoc.locator('nth=0').waitFor() // nth=0 bc error if there's more than 1
47+
48+
const rowLoc = table.locator('tbody >> role=row')
49+
await rowLoc.locator('nth=0').waitFor()
50+
51+
const headerKeys = await map(
52+
table.locator('thead >> role=cell'),
53+
async (cell) => await cell.textContent()
54+
)
55+
56+
const rows = await map(table.locator('tbody >> role=row'), async (row) => {
57+
const rowPairs = await map(row.locator('role=cell'), async (cell, i) => [
58+
headerKeys[i],
59+
// accessible name would be better but it's not in yet
60+
// https://github.com/microsoft/playwright/issues/13517
61+
await cell.textContent(),
62+
])
63+
return Object.fromEntries(rowPairs.filter(([k]) => k && k.length > 0))
64+
})
65+
66+
await expect(rows).toEqual(expect.arrayContaining([expect.objectContaining(expectedRow)]))
4667
}

libs/api-mocks/msw/handlers.ts

+16
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,22 @@ export const handlers = [
462462
}
463463
),
464464

465+
rest.get<never, InstanceParams, Json<Api.ExternalIpResultsPage> | GetErr>(
466+
'/api/organizations/:orgName/projects/:projectName/instances/:instanceName/external-ips',
467+
(req, res) => {
468+
const [, err] = lookupInstance(req.params)
469+
if (err) return res(err)
470+
// TODO: proper mock table
471+
const items = [
472+
{
473+
ip: '123.4.56.7',
474+
kind: 'ephemeral',
475+
} as const,
476+
]
477+
return res(json({ items }))
478+
}
479+
),
480+
465481
rest.get<never, InstanceParams, Json<Api.NetworkInterfaceResultsPage> | GetErr>(
466482
'/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces',
467483
(req, res) => {

libs/api/__generated__/Api.ts

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/api/__generated__/OMICRON_VERSION

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)