Skip to content

Commit 3b574e5

Browse files
authored
ListPlusCell for firewall rule filters (#2163)
* configurable numInCell on ListPlusCell, use for firewall rule filters * use it on targets too, add mock data for that * get clever about numInCell * test overflow a bit more
1 parent b8c79ca commit 3b574e5

File tree

6 files changed

+145
-47
lines changed

6 files changed

+145
-47
lines changed

app/components/ListPlusCell.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,30 @@ import { Tooltip } from '~/ui/lib/Tooltip'
1313
type ListPlusCellProps = {
1414
tooltipTitle: string
1515
children: React.ReactNode
16+
/** The number of items to show in the cell vs. in the popup */
17+
numInCell?: number
1618
}
1719

1820
/**
1921
* Gives a count with a tooltip that expands to show details when the user hovers over it
2022
*/
21-
export const ListPlusCell = ({ tooltipTitle, children }: ListPlusCellProps) => {
22-
const [first, ...rest] = React.Children.toArray(children)
23+
export const ListPlusCell = ({
24+
tooltipTitle,
25+
children,
26+
numInCell = 1,
27+
}: ListPlusCellProps) => {
28+
const array = React.Children.toArray(children)
29+
const inCell = array.slice(0, numInCell)
30+
const rest = array.slice(numInCell)
2331
const content = (
2432
<div>
2533
<div className="mb-2">{tooltipTitle}</div>
26-
{...rest}
34+
<div className="flex flex-col items-start gap-2">{...rest}</div>
2735
</div>
2836
)
2937
return (
3038
<div className="flex items-baseline gap-2">
31-
{first}
39+
{inCell}
3240
{rest.length > 0 && (
3341
<Tooltip content={content} placement="bottom">
3442
<div className="text-mono-sm">+{rest.length}</div>

app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ import {
1515
type VpcFirewallRule,
1616
} from '@oxide/api'
1717

18+
import { ListPlusCell } from '~/components/ListPlusCell'
1819
import { CreateFirewallRuleForm } from '~/forms/firewall-rules-create'
1920
import { EditFirewallRuleForm } from '~/forms/firewall-rules-edit'
2021
import { useVpcSelector } from '~/hooks'
2122
import { confirmDelete } from '~/stores/confirm-delete'
2223
import { EnabledCell } from '~/table/cells/EnabledCell'
23-
import { FirewallFilterCell } from '~/table/cells/FirewallFilterCell'
2424
import { ButtonCell } from '~/table/cells/LinkCell'
2525
import { TypeValueCell } from '~/table/cells/TypeValueCell'
2626
import { getActionsCol } from '~/table/columns/action-col'
2727
import { Columns } from '~/table/columns/common'
2828
import { Table } from '~/table/Table'
29+
import { Badge } from '~/ui/lib/Badge'
2930
import { CreateButton } from '~/ui/lib/CreateButton'
3031
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
3132
import { TableEmptyBox } from '~/ui/lib/Table'
@@ -50,17 +51,41 @@ const staticColumns = [
5051
}),
5152
colHelper.accessor('targets', {
5253
header: 'Targets',
53-
cell: (info) => (
54-
<div>
55-
{info.getValue().map(({ type, value }) => (
56-
<TypeValueCell key={type + '|' + value} type={type} value={value} />
57-
))}
58-
</div>
59-
),
54+
cell: (info) => {
55+
const targets = info.getValue()
56+
const children = targets.map(({ type, value }) => (
57+
<TypeValueCell key={type + '|' + value} type={type} value={value} />
58+
))
59+
// if there's going to be overflow anyway, might as well make the cell narrow
60+
const numInCell = children.length <= 2 ? 2 : 1
61+
return (
62+
<ListPlusCell numInCell={numInCell} tooltipTitle="Other targets">
63+
{info.getValue().map(({ type, value }) => (
64+
<TypeValueCell key={type + '|' + value} type={type} value={value} />
65+
))}
66+
</ListPlusCell>
67+
)
68+
},
6069
}),
6170
colHelper.accessor('filters', {
6271
header: 'Filters',
63-
cell: (info) => <FirewallFilterCell {...info.getValue()} />,
72+
cell: (info) => {
73+
const { hosts, ports, protocols } = info.getValue()
74+
const children = [
75+
...(hosts || []).map((tv, i) => <TypeValueCell key={`${tv}-${i}`} {...tv} />),
76+
...(protocols || []).map((p, i) => <Badge key={`${p}-${i}`}>{p}</Badge>),
77+
...(ports || []).map((p, i) => (
78+
<TypeValueCell key={`port-${p}-${i}`} type="Port" value={p} />
79+
)),
80+
]
81+
// if there's going to be overflow anyway, might as well make the cell narrow
82+
const numInCell = children.length <= 2 ? 2 : 1
83+
return (
84+
<ListPlusCell numInCell={numInCell} tooltipTitle="Other filters">
85+
{children}
86+
</ListPlusCell>
87+
)
88+
},
6489
}),
6590
colHelper.accessor('status', {
6691
header: 'Status',
@@ -149,7 +174,7 @@ export const VpcFirewallRulesTab = () => {
149174
/>
150175
)}
151176
</div>
152-
{rules.length > 0 ? <Table table={table} rowHeight="large" /> : emptyState}
177+
{rules.length > 0 ? <Table table={table} /> : emptyState}
153178
</>
154179
)
155180
}

app/table/cells/FirewallFilterCell.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

mock-api/msw/db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ const initDb = {
286286
snapshots: [...mock.snapshots],
287287
sshKeys: [...mock.sshKeys],
288288
users: [...mock.users],
289-
vpcFirewallRules: [...mock.defaultFirewallRules],
290-
vpcs: [mock.vpc],
289+
vpcFirewallRules: [...mock.firewallRules],
290+
vpcs: [...mock.vpcs],
291291
vpcSubnets: [mock.vpcSubnet],
292292
}
293293

mock-api/vpc.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import type { Vpc, VpcFirewallRule, VpcSubnet } from '@oxide/api'
1010

1111
import type { Json } from './json-type'
12-
import { project } from './project'
12+
import { project, project2 } from './project'
1313

1414
const time_created = new Date(2021, 0, 1).toISOString()
1515
const time_modified = new Date(2021, 0, 2).toISOString()
@@ -28,6 +28,20 @@ export const vpc: Json<Vpc> = {
2828
time_modified,
2929
}
3030

31+
export const vpc2: Json<Vpc> = {
32+
id: 'e54078df-fe72-4673-b36c-a362e3b4e38b',
33+
name: 'mock-vpc-2',
34+
description: 'a fake vpc',
35+
dns_name: 'mock-vpc-2',
36+
project_id: project2.id,
37+
system_router_id: systemRouterId,
38+
ipv6_prefix: 'fdf6:1818:b6e2::/48',
39+
time_created,
40+
time_modified,
41+
}
42+
43+
export const vpcs: Json<Vpc[]> = [vpc, vpc2]
44+
3145
export const vpcSubnet: Json<VpcSubnet> = {
3246
// this is supposed to be flattened into the top level. will fix in API
3347
id: 'd12bf934-d2bf-40e9-8596-bb42a7793749',
@@ -49,7 +63,7 @@ export const vpcSubnet2: Json<VpcSubnet> = {
4963
ipv4_block: '10.1.1.2/24',
5064
}
5165

52-
export const defaultFirewallRules: Json<VpcFirewallRule[]> = [
66+
export const firewallRules: Json<VpcFirewallRule[]> = [
5367
{
5468
id: 'b74aeea8-1201-4efd-b6ec-011f10a0b176',
5569
name: 'allow-internal-inbound',
@@ -117,4 +131,46 @@ export const defaultFirewallRules: Json<VpcFirewallRule[]> = [
117131
time_modified,
118132
vpc_id: vpc.id,
119133
},
134+
// second mock VPC in other project, meant to test display with lots of
135+
// targets and filters
136+
{
137+
id: '097c849e-68c8-43f7-9ceb-b1855c51f178',
138+
name: 'lots-of-filters',
139+
status: 'enabled',
140+
direction: 'inbound',
141+
targets: [{ type: 'vpc', value: 'default' }],
142+
description: 'we just want to test with lots of filters',
143+
filters: {
144+
ports: ['3389', '45-89'],
145+
protocols: ['TCP'],
146+
hosts: [
147+
{ type: 'instance', value: 'hello-friend' },
148+
{ type: 'subnet', value: 'my-subnet' },
149+
{ type: 'ip', value: '148.38.89.5' },
150+
],
151+
},
152+
action: 'allow',
153+
priority: 65534,
154+
time_created,
155+
time_modified,
156+
vpc_id: vpc2.id,
157+
},
158+
{
159+
id: '097c849e-68c8-43f7-9ceb-b1855c51f178',
160+
name: 'lots-of-targets',
161+
status: 'enabled',
162+
direction: 'inbound',
163+
targets: [
164+
{ type: 'instance', value: 'my-inst' },
165+
{ type: 'ip', value: '125.34.25.2' },
166+
{ type: 'subnet', value: 'subsubsub' },
167+
],
168+
description: 'we just want to test with lots of targets',
169+
filters: { ports: ['80'] },
170+
action: 'allow',
171+
priority: 65534,
172+
time_created,
173+
time_modified,
174+
vpc_id: vpc2.id,
175+
},
120176
]

test/e2e/firewall-rules.e2e.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,52 @@ test('can create firewall rule', async ({ page }) => {
9090
Name: 'my-new-rule',
9191
Priority: '5',
9292
Targets: 'ip192.168.0.1',
93-
Filters: 'instancehost-filter-instanceUDP123-456',
93+
Filters: 'instancehost-filter-instance+2', // UDP and port filters in plus popup
9494
})
9595

96+
// scroll table sideways past the filters cell
97+
await page.getByText('Enabled').first().scrollIntoViewIfNeeded()
98+
99+
await page.getByText('+2').hover()
100+
const tooltip = page.getByRole('tooltip', { name: 'Other filters UDP Port 123-' })
101+
await expect(tooltip).toBeVisible()
102+
96103
await expect(rows).toHaveCount(5)
97104
for (const name of defaultRules) {
98105
await expect(page.locator(`text="${name}"`)).toBeVisible()
99106
}
100107
})
101108

109+
test('firewall rule targets and filters overflow', async ({ page }) => {
110+
await page.goto('/projects/other-project/vpcs/mock-vpc-2')
111+
112+
await expect(
113+
page.getByRole('cell', { name: 'instance my-inst +2', exact: true })
114+
).toBeVisible()
115+
116+
await page.getByText('+2').hover()
117+
await expect(
118+
page.getByRole('tooltip', {
119+
name: 'Other targets ip 125.34.25.2 subnet subsubsub',
120+
exact: true,
121+
})
122+
).toBeVisible()
123+
124+
await expect(
125+
page.getByRole('cell', { name: 'instance hello-friend +5', exact: true })
126+
).toBeVisible()
127+
128+
// scroll table sideways past the filters cell
129+
await page.getByText('Enabled').first().scrollIntoViewIfNeeded()
130+
131+
await page.getByText('+5').hover()
132+
const tooltip = page.getByRole('tooltip', {
133+
name: 'Other filters subnet my-subnet ip 148.38.89.5 TCP Port 3389 Port 45-89',
134+
exact: true,
135+
})
136+
await expect(tooltip).toBeVisible()
137+
})
138+
102139
test('firewall rule form targets table', async ({ page }) => {
103140
await page.goto('/projects/mock-project/vpcs/mock-vpc')
104141
await page.getByRole('tab', { name: 'Firewall Rules' }).click()

0 commit comments

Comments
 (0)