diff --git a/app/api/util.ts b/app/api/util.ts index fb763d494..ea227eb65 100644 --- a/app/api/util.ts +++ b/app/api/util.ts @@ -132,9 +132,9 @@ const instanceActions = { // https://github.com/oxidecomputer/omicron/blob/6dd9802/nexus/src/app/instance.rs#L1520-L1522 serialConsole: ['running', 'rebooting', 'migrating', 'repairing'], - // https://github.com/oxidecomputer/omicron/blob/5e27bde/nexus/src/app/affinity.rs#L357 checks to see that there's no VMM - // TODO: determine whether the intent is only `stopped` or also `failed` - addToAntiAffinityGroup: ['stopped'], + // check to see that there's no VMM + // https://github.com/oxidecomputer/omicron/blob/c496683/nexus/db-queries/src/db/datastore/affinity.rs#L1025-L1034 + addToAffinityGroup: ['stopped'], } satisfies Record // setting .states is a cute way to make it ergonomic to call the test function diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx index d6fe97e35..00498b5ff 100644 --- a/app/forms/anti-affinity-group-member-add.tsx +++ b/app/forms/anti-affinity-group-member-add.tsx @@ -9,12 +9,13 @@ import { useId } from 'react' import { useForm } from 'react-hook-form' -import { queryClient, useApiMutation, type Instance } from '~/api' +import { instanceCan, queryClient, useApiMutation, type Instance } from '~/api' import { ComboboxField } from '~/components/form/fields/ComboboxField' import { HL } from '~/components/HL' import { useAntiAffinityGroupSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { toComboboxItems } from '~/ui/lib/Combobox' +import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' type Values = { instance: string } @@ -45,6 +46,12 @@ export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }: }) }) + const instance = form.watch('instance') + const selectedInstance = instances.find((i) => i.name === instance) + const canAddInstance = selectedInstance + ? instanceCan.addToAffinityGroup(selectedInstance) + : false + return ( @@ -63,9 +70,20 @@ export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }: control={form.control} /> + {!canAddInstance && ( + + )} - + ) } diff --git a/app/pages/project/instances/AntiAffinityCard.tsx b/app/pages/project/instances/AntiAffinityCard.tsx index fdaa4606c..d9accb606 100644 --- a/app/pages/project/instances/AntiAffinityCard.tsx +++ b/app/pages/project/instances/AntiAffinityCard.tsx @@ -152,7 +152,7 @@ export function AntiAffinityCard() { }) let disabledReason = undefined - if (!instanceCan.addToAntiAffinityGroup(instanceData)) { + if (!instanceCan.addToAffinityGroup(instanceData)) { disabledReason = <>Only stopped instances can be added to a group // prettier-ignore } else if (allGroups.items.length === 0) { diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts index 7e2a62388..34daae82b 100644 --- a/test/e2e/anti-affinity.e2e.ts +++ b/test/e2e/anti-affinity.e2e.ts @@ -59,6 +59,7 @@ test('can add a new anti-affinity group', async ({ page }) => { const addInstanceButton = page.getByRole('button', { name: 'Add instance' }) const addInstanceModal = page.getByRole('dialog', { name: 'Add instance to group' }) const instanceCombobox = page.getByRole('combobox', { name: 'Instance' }) + const modalAddButton = page.getByRole('button', { name: 'Add to group' }) // open modal and pick instance await addInstanceButton.click() @@ -72,10 +73,30 @@ test('can add a new anti-affinity group', async ({ page }) => { await expect(addInstanceModal).toBeHidden() await addInstanceButton.click() await expect(instanceCombobox).toHaveValue('') + await page.getByRole('option', { name: 'db1' }).click() + // the submit button should be disabled + await expect(modalAddButton).toBeDisabled() + + // go disable db1 + await page.getByRole('button', { name: 'Cancel' }).click() + await page.getByRole('link', { name: 'Instances' }).click() + clickRowAction(page, 'db1', 'Stop') + await page.getByRole('button', { name: 'Confirm' }).click() + await expectRowVisible(page.getByRole('table'), { + name: 'db1', + state: expect.stringContaining('stopped'), + }) - // now do it again for real and submit + // go back to the anti-affinity group and add the instance + await page.getByRole('link', { name: 'Affinity' }).click() + await page.getByRole('link', { name: 'new-anti-affinity-group' }).click() + await addInstanceButton.click() + await expect(addInstanceModal).toBeVisible() + await instanceCombobox.fill('db1') await page.getByRole('option', { name: 'db1' }).click() - await page.getByRole('button', { name: 'Add to group' }).click() + await expect(instanceCombobox).toHaveValue('db1') + // the submit button should be enabled + await modalAddButton.click() const cell = page.getByRole('cell', { name: 'db1' }) await expect(cell).toBeVisible()