Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
32d18ad
Enable removal of group member
charliepark Mar 27, 2025
429eda6
Enable deletion of group
charliepark Mar 27, 2025
eb387a0
move away from useMemo for columns
charliepark Mar 27, 2025
16795fc
Update copy in remove confirm modal
charliepark Mar 27, 2025
3b2db66
Update copy in delete modal
charliepark Mar 28, 2025
58e6e6b
use apiq since we're not paginating
david-crespo Mar 28, 2025
d42ad18
use the id for delete
david-crespo Mar 28, 2025
067f9ff
merged main and reconciling diffs
charliepark Mar 28, 2025
6e2249c
Add new anti-affinity group
charliepark Apr 1, 2025
ffd7cb0
ah … crumbs
charliepark Apr 1, 2025
7456008
Edit anti-affinity group is working
charliepark Apr 1, 2025
e837d9d
Fix bug; add ID column to table
charliepark Apr 1, 2025
e93d506
can add instances to an anti-affinity group
charliepark Apr 2, 2025
bb1c4cc
merge main and resolve conflicts
charliepark Apr 2, 2025
3511e0d
Update to ID column truncation
charliepark Apr 2, 2025
271b854
Refactoring
charliepark Apr 2, 2025
edd0456
Missed a spot in the refactoring
charliepark Apr 2, 2025
c307e72
Update snapshots
charliepark Apr 3, 2025
b31c1ca
Can just use prefetchQuery, since we don't need returned data yet
charliepark Apr 3, 2025
d1f7945
reorder functions
charliepark Apr 3, 2025
97b0137
move instanceList fetching up a level; use to disable button when no …
charliepark Apr 3, 2025
e6d996b
Use existing types for forms
charliepark Apr 3, 2025
7486186
export function as default
charliepark Apr 3, 2025
829344b
refactor idCell
charliepark Apr 3, 2025
a54352d
Update mock-api/msw/handlers.ts
charliepark Apr 3, 2025
3d2ae30
don't reuse AffinityPageHeader
charliepark Apr 3, 2025
72efb00
Shorter button copy; new page header
charliepark Apr 3, 2025
5e34196
Don't include sorting; already present in actual data
charliepark Apr 3, 2025
3ae6cf9
Try 'anti-affinity' as header / nav link
charliepark Apr 3, 2025
666230d
Clean up copy button a bit
charliepark Apr 3, 2025
2a99627
More clever disabledReason; needs max member verification
charliepark Apr 3, 2025
22978df
use regular link for group edit row action
david-crespo Apr 3, 2025
cc9875a
put it back to Affinity title
david-crespo Apr 3, 2025
00cc029
draft docs popover
david-crespo Apr 3, 2025
5e5b3ee
A few more refactors / PR comments
charliepark Apr 3, 2025
3fe3fc2
routing fix
charliepark Apr 3, 2025
8bcb26b
reintroduce convert in routes for group create
charliepark Apr 3, 2025
36884e5
update max members value
charliepark Apr 3, 2025
8e5e31c
Link to specific commit for line reference stability
charliepark Apr 3, 2025
e27d854
Add e2e tests
charliepark Apr 4, 2025
ff31223
Refresh of Affinity Groups table columns; use count in place of insta…
charliepark Apr 4, 2025
4c7f8f3
update test
charliepark Apr 4, 2025
ee4e2f1
don't fetch affinity groups
david-crespo Apr 4, 2025
fb18dd3
members col -> instances, don't validate name uniqueness
david-crespo Apr 4, 2025
f635780
add delete and docs popover to group detail, use confirmDelete
david-crespo Apr 4, 2025
ff32750
help text on policy field and tip icon on policy columns
david-crespo Apr 4, 2025
872feac
merge main and resolve conflicts with AffinityGroupPolicyBadge
charliepark Apr 4, 2025
08b81e1
put back the line about policy in the popover
david-crespo Apr 4, 2025
ee98abb
remove title from icons in sidebar nav
david-crespo Apr 4, 2025
5a58312
Refactoring form in add instance to A-A group modal
charliepark Apr 5, 2025
99a2c78
type -> group type, remove description column
david-crespo Apr 7, 2025
6f21f11
on second thought: make page title Affinity Groups
david-crespo Apr 7, 2025
064879e
simplify form reset by unmounting, test that in e2e
david-crespo Apr 7, 2025
16bb032
use handleSubmit higher so we don't have to type explicitly
david-crespo Apr 7, 2025
37a6733
make enter submit add instance modal form
david-crespo Apr 7, 2025
42f772e
link to instance settings rather than default tab
david-crespo Apr 7, 2025
696484e
hopefully final policy help copy tweaks
david-crespo Apr 7, 2025
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
29 changes: 29 additions & 0 deletions app/components/AffinityDocsPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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
*/
import { Affinity16Icon } from '@oxide/design-system/icons/react'

import { policyHelpText } from '~/forms/affinity-util'
import { TipIcon } from '~/ui/lib/TipIcon'
import { docLinks } from '~/util/links'

import { DocsPopover } from './DocsPopover'

export const AffinityDocsPopover = () => (
<DocsPopover
heading="affinity"
icon={<Affinity16Icon />}
summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy attribute determines whether instances can still start when a unique sled is not available."
links={[docLinks.affinity]}
/>
)

export const AffinityPolicyHeader = () => (
<>
Policy<TipIcon className="ml-1.5">{policyHelpText}</TipIcon>
</>
)
36 changes: 36 additions & 0 deletions app/forms/affinity-util.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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
*/

import { apiq } from '~/api'
import { ALL_ISH } from '~/util/consts'
import type * as PP from '~/util/path-params'

export const instanceList = ({ project }: PP.Project) =>
apiq('instanceList', { query: { project, limit: ALL_ISH } })

export const antiAffinityGroupList = ({ project }: PP.Project) =>
apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })

export const antiAffinityGroupView = ({
project,
antiAffinityGroup,
}: PP.AntiAffinityGroup) =>
apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })

export const antiAffinityGroupMemberList = ({
antiAffinityGroup,
project,
}: PP.AntiAffinityGroup) =>
apiq('antiAffinityGroupMemberList', {
path: { antiAffinityGroup },
// member limit in DB is currently 32, so pagination isn't needed
query: { project, limit: ALL_ISH },
})

export const policyHelpText =
"Determines whether member instances are allowed to start when the anti-affinity rule can't be satisfied"
81 changes: 81 additions & 0 deletions app/forms/anti-affinity-group-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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
*/
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router'

import { queryClient, useApiMutation, type AntiAffinityGroupCreate } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { RadioField } from '~/components/form/fields/RadioField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

import { policyHelpText } from './affinity-util'

export const handle = titleCrumb('New anti-affinity group')

const defaultValues: Omit<AntiAffinityGroupCreate, 'failureDomain'> = {
name: '',
description: '',
policy: 'allow',
}

export default function CreateAntiAffinityGroupForm() {
const { project } = useProjectSelector()

const navigate = useNavigate()

const createAntiAffinityGroup = useApiMutation('antiAffinityGroupCreate', {
onSuccess(antiAffinityGroup) {
queryClient.invalidateEndpoint('antiAffinityGroupList')
navigate(pb.antiAffinityGroup({ project, antiAffinityGroup: antiAffinityGroup.name }))
addToast(<>Anti-affinity group <HL>{antiAffinityGroup.name}</HL> created</>) // prettier-ignore
},
})

const form = useForm({ defaultValues })
const control = form.control

return (
<SideModalForm
form={form}
formType="create"
resourceName="rule"
title="Add anti-affinity group"
onDismiss={() => navigate(pb.affinity({ project }))}
onSubmit={(values) =>
createAntiAffinityGroup.mutate({
query: { project },
body: { ...values, failureDomain: 'sled' },
})
}
loading={createAntiAffinityGroup.isPending}
submitError={createAntiAffinityGroup.error}
submitLabel="Add group"
>
<NameField name="name" control={control} />
<DescriptionField name="description" control={control} />
<RadioField
name="policy"
// forgive me
description={`${policyHelpText}, i.e., when all available sleds already contain a group member.`}
column
control={control}
items={[
{ value: 'allow', label: 'Allow' },
{ value: 'fail', label: 'Fail' },
]}
/>
</SideModalForm>
)
}
84 changes: 84 additions & 0 deletions app/forms/anti-affinity-group-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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
*/
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
import * as R from 'remeda'

import {
queryClient,
useApiMutation,
usePrefetchedQuery,
type AntiAffinityGroupUpdate,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import {
getAntiAffinityGroupSelector,
useAntiAffinityGroupSelector,
} from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

import { antiAffinityGroupView } from './affinity-util'

export const handle = titleCrumb('New anti-affinity group')

export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project, antiAffinityGroup } = getAntiAffinityGroupSelector(params)
await queryClient.prefetchQuery(antiAffinityGroupView({ project, antiAffinityGroup }))
return null
}

export default function EditAntiAffintyGroupForm() {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()

const navigate = useNavigate()

const editAntiAffinityGroup = useApiMutation('antiAffinityGroupUpdate', {
onSuccess(updatedGroup) {
queryClient.invalidateEndpoint('antiAffinityGroupView')
queryClient.invalidateEndpoint('antiAffinityGroupList')
navigate(pb.antiAffinityGroup({ project, antiAffinityGroup: updatedGroup.name }))
addToast(<>Anti-affinity group <HL>{updatedGroup.name}</HL> updated</>) // prettier-ignore
},
})

const { data: group } = usePrefetchedQuery(
antiAffinityGroupView({ project, antiAffinityGroup })
)

const defaultValues: AntiAffinityGroupUpdate = R.pick(group, ['name', 'description'])
const form = useForm({ defaultValues })

return (
<SideModalForm
form={form}
formType="create"
resourceName="rule"
title="Edit anti-affinity group"
onDismiss={() => navigate(pb.antiAffinityGroup({ project, antiAffinityGroup }))}
onSubmit={(values) => {
editAntiAffinityGroup.mutate({
path: { antiAffinityGroup },
query: { project },
body: values,
})
}}
loading={editAntiAffinityGroup.isPending}
submitError={editAntiAffinityGroup.error}
submitLabel="Edit group"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
71 changes: 71 additions & 0 deletions app/forms/anti-affinity-group-member-add.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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
*/

import { useId } from 'react'
import { useForm } from 'react-hook-form'

import { 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 { Modal } from '~/ui/lib/Modal'

type Values = { instance: string }

const defaultValues: Values = { instance: '' }

type Props = { instances: Instance[]; onDismiss: () => void }

export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }: Props) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()

const form = useForm({ defaultValues })
const formId = useId()

const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
onSuccess(_data, variables) {
onDismiss()
queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
queryClient.invalidateEndpoint('antiAffinityGroupView')
addToast(<>Instance <HL>{variables.path.instance}</HL> added to anti-affinity group <HL>{antiAffinityGroup}</HL></>) // prettier-ignore
},
})

const onSubmit = form.handleSubmit(({ instance }) => {
addMember({
path: { antiAffinityGroup, instance },
query: { project },
})
})

return (
<Modal isOpen onDismiss={onDismiss} title="Add instance to group">
<Modal.Body>
<Modal.Section>
<p className="text-sm text-gray-500">
Select an instance to add to the anti-affinity group{' '}
<HL>{antiAffinityGroup}</HL>. Only stopped instances can be added to the group.
</p>
<form id={formId} onSubmit={onSubmit}>
<ComboboxField
placeholder="Select an instance"
name="instance"
label="Instance"
items={toComboboxItems(instances)}
required
control={form.control}
/>
</form>
</Modal.Section>
</Modal.Body>
<Modal.Footer onDismiss={onDismiss} actionText="Add to group" formId={formId} />
</Modal>
)
}
8 changes: 4 additions & 4 deletions app/layouts/ProjectLayoutBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
{ value: 'Affinity', path: pb.affinity(projectSelector) },
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
{ value: 'Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
Expand Down Expand Up @@ -106,7 +106,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
<Snapshots16Icon /> Snapshots
</NavLinkItem>
<NavLinkItem to={pb.projectImages(projectSelector)}>
<Images16Icon title="images" /> Images
<Images16Icon /> Images
</NavLinkItem>
<NavLinkItem to={pb.vpcs(projectSelector)}>
<Networking16Icon /> VPCs
Expand All @@ -115,10 +115,10 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
<IpGlobal16Icon /> Floating IPs
</NavLinkItem>
<NavLinkItem to={pb.affinity(projectSelector)}>
<Affinity16Icon title="Affinity" /> Affinity
<Affinity16Icon /> Affinity Groups
</NavLinkItem>
<NavLinkItem to={pb.projectAccess(projectSelector)}>
<Access16Icon title="Access" /> Access
<Access16Icon /> Access
</NavLinkItem>
</Sidebar.Nav>
</Sidebar>
Expand Down
Loading
Loading