Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 23 additions & 8 deletions app/pages/project/instances/NetworkingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,30 @@ export default function NetworkingTab() {
query: { project },
})

const nics = usePrefetchedApiQuery('instanceNetworkInterfaceList', {
query: { ...instanceSelector, limit: ALL_ISH },
}).data.items

const multipleNics = nics.length > 1

const makeActions = useCallback(
(nic: NicRow): MenuAction[] => {
const canUpdateNic = instanceCan.updateNic({ runState: nic.instanceState })

const deleteDisabledReason = () => {
if (!canUpdateNic) {
return <>The instance must be {updateNicStates} to delete a network interface</>
}
// If the NIC is primary, we can't delete it if there are other NICs. Per Ben N:
// > There is always zero or one primary NIC. There may zero or more secondary NICs (up to 7 today), but only if there is already a primary.
// > The primary NIC is where we attach all the external networking state, like external addresses, and the VPC information like routes, subnet information, internet gateways, etc.
// > You may delete any secondary NIC. You may delete the primary NIC only if it's the only NIC (there are no secondary NICs).
if (nic.primary && multipleNics) {
return 'The primary interface can’t be deleted while other interfaces are attached. To delete it, make another interface primary.'
}
return undefined
}

return [
{
label: 'Make primary',
Expand Down Expand Up @@ -266,21 +287,15 @@ export default function NetworkingTab() {
}),
label: nic.name,
}),
disabled: !canUpdateNic && (
<>The instance must be {updateNicStates} to delete a network interface</>
),
disabled: deleteDisabledReason(),
},
]
},
[deleteNic, editNic, instanceSelector]
[deleteNic, editNic, instanceSelector, multipleNics]
)

const columns = useColsWithActions(staticCols, makeActions)

const nics = usePrefetchedApiQuery('instanceNetworkInterfaceList', {
query: { ...instanceSelector, limit: ALL_ISH },
}).data.items

const nicRows = useMemo(
() => nics.map((nic) => ({ ...nic, instanceState: instance.runState })),
[nics, instance]
Expand Down
26 changes: 24 additions & 2 deletions test/e2e/instance-networking.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
*/
import { expect, test } from '@playwright/test'

import { clickRowAction, expectRowVisible, expectVisible, stopInstance } from './utils'
import {
clickRowAction,
clickRowActions,
expectRowVisible,
expectVisible,
stopInstance,
} from './utils'

test('Instance networking tab — NIC table', async ({ page }) => {
await page.goto('/projects/mock-project/instances/db1')
Expand Down Expand Up @@ -72,7 +78,23 @@ test('Instance networking tab — NIC table', async ({ page }) => {
const nic3 = page.getByRole('cell', { name: 'nic-3' })
await expect(nic3).toBeVisible()

// Delete just-added network interface
// See that the primary NIC cannot be deleted when other NICs exist
await clickRowActions(page, 'nic-3')
const deleteButton = page.getByRole('menuitem', { name: 'Delete' })
await expect(deleteButton).toBeDisabled()
await deleteButton.hover()
await expect(page.getByText('The primary interface can’t')).toBeVisible()

// close the menu for nic-3, without the next line fails in FF and Safari (but not Chrome)
await clickRowActions(page, 'nic-3')

// Delete the non-primary NIC
await clickRowAction(page, 'my-nic', 'Delete')
await expect(page.getByText('Are you sure you want to delete my-nic?')).toBeVisible()
await page.getByRole('button', { name: 'Confirm' }).click()
await expect(page.getByRole('cell', { name: 'my-nic' })).toBeHidden()

// Now the primary NIC is deletable
await clickRowAction(page, 'nic-3', 'Delete')
await page.getByRole('button', { name: 'Confirm' }).click()
await expect(nic3).toBeHidden()
Expand Down
18 changes: 9 additions & 9 deletions test/e2e/instance.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/
import {
clickRowAction,
clickRowActions,
closeToast,
expect,
expectRowVisible,
openRowActions,
test,
type Page,
} from './utils'
Expand Down Expand Up @@ -41,7 +41,7 @@ test('can start a failed instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')

// check the start button disabled message on a running instance
await openRowActions(page, 'db1')
await clickRowActions(page, 'db1')
await page.getByRole('menuitem', { name: 'Start' }).hover()
await expect(
page.getByText('Only stopped or failed instances can be started')
Expand Down Expand Up @@ -118,14 +118,14 @@ test('can reboot a running instance', async ({ page }) => {
test('cannot reboot a failed instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')
await expectInstanceState(page, 'you-fail', 'failed')
await openRowActions(page, 'you-fail')
await clickRowActions(page, 'you-fail')
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
})

test('cannot reboot a starting instance, or a stopped instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')
await expectInstanceState(page, 'not-there-yet', 'starting')
await openRowActions(page, 'not-there-yet')
await clickRowActions(page, 'not-there-yet')
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
// hit escape to close the menu so clickRowAction succeeds
await page.keyboard.press('Escape')
Expand All @@ -136,21 +136,21 @@ test('cannot reboot a starting instance, or a stopped instance', async ({ page }
await expectInstanceState(page, 'not-there-yet', 'stopping')
await expectInstanceState(page, 'not-there-yet', 'stopped')
// reboot is still disabled for a stopped instance
await openRowActions(page, 'not-there-yet')
await clickRowActions(page, 'not-there-yet')
await expect(page.getByRole('menuitem', { name: 'Reboot' })).toBeDisabled()
})

test('cannot resize a running or starting instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')

await expectInstanceState(page, 'db1', 'running')
await openRowActions(page, 'db1')
await clickRowActions(page, 'db1')
await expect(page.getByRole('menuitem', { name: 'Resize' })).toBeDisabled()

await page.keyboard.press('Escape') // get out of the menu

await expectInstanceState(page, 'not-there-yet', 'starting')
await openRowActions(page, 'not-there-yet')
await clickRowActions(page, 'not-there-yet')
await expect(page.getByRole('menuitem', { name: 'Resize' })).toBeDisabled()
})

Expand Down Expand Up @@ -254,7 +254,7 @@ async function expectRowMenuStaysOpen(page: Page, rowSelector: string) {
await expect(menu).toBeHidden()
await expect(stopped).toBeHidden()

await openRowActions(page, rowSelector)
await clickRowActions(page, rowSelector)
await expect(stopped).toBeHidden() // still not stopped yet
await expect(menu).toBeVisible()

Expand Down Expand Up @@ -300,7 +300,7 @@ test("polling doesn't close row actions: instances", async ({ page }) => {
await expect(menu).toBeHidden()
await expect(stopped).toBeHidden()

await openRowActions(page, 'db1')
await clickRowActions(page, 'db1')
await expect(stopped).toBeHidden() // still not stopped yet
await expect(menu).toBeVisible()

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export async function closeToast(page: Page) {
export const clipboardText = async (page: Page) =>
page.evaluate(() => navigator.clipboard.readText())

export const openRowActions = async (page: Page, name: string) => {
export const clickRowActions = async (page: Page, name: string) => {
await page
.getByRole('row', { name, exact: false })
.getByRole('button', { name: 'Row actions' })
Expand All @@ -163,7 +163,7 @@ export const openRowActions = async (page: Page, name: string) => {

/** Select row by `rowName`, click the row actions button, and click `actionName` */
export async function clickRowAction(page: Page, rowName: string, actionName: string) {
await openRowActions(page, rowName)
await clickRowActions(page, rowName)
await page.getByRole('menuitem', { name: actionName }).click()
}

Expand Down
Loading