-
Notifications
You must be signed in to change notification settings - Fork 19
Add create and edit functionality to anti-affinity groups #2775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 429eda6
Enable deletion of group
charliepark eb387a0
move away from useMemo for columns
charliepark 16795fc
Update copy in remove confirm modal
charliepark 3b2db66
Update copy in delete modal
charliepark 58e6e6b
use apiq since we're not paginating
david-crespo d42ad18
use the id for delete
david-crespo 067f9ff
merged main and reconciling diffs
charliepark 6e2249c
Add new anti-affinity group
charliepark ffd7cb0
ah … crumbs
charliepark 7456008
Edit anti-affinity group is working
charliepark e837d9d
Fix bug; add ID column to table
charliepark e93d506
can add instances to an anti-affinity group
charliepark bb1c4cc
merge main and resolve conflicts
charliepark 3511e0d
Update to ID column truncation
charliepark 271b854
Refactoring
charliepark edd0456
Missed a spot in the refactoring
charliepark c307e72
Update snapshots
charliepark b31c1ca
Can just use prefetchQuery, since we don't need returned data yet
charliepark d1f7945
reorder functions
charliepark 97b0137
move instanceList fetching up a level; use to disable button when no …
charliepark e6d996b
Use existing types for forms
charliepark 7486186
export function as default
charliepark 829344b
refactor idCell
charliepark a54352d
Update mock-api/msw/handlers.ts
charliepark 3d2ae30
don't reuse AffinityPageHeader
charliepark 72efb00
Shorter button copy; new page header
charliepark 5e34196
Don't include sorting; already present in actual data
charliepark 3ae6cf9
Try 'anti-affinity' as header / nav link
charliepark 666230d
Clean up copy button a bit
charliepark 2a99627
More clever disabledReason; needs max member verification
charliepark 22978df
use regular link for group edit row action
david-crespo cc9875a
put it back to Affinity title
david-crespo 00cc029
draft docs popover
david-crespo 5e5b3ee
A few more refactors / PR comments
charliepark 3fe3fc2
routing fix
charliepark 8bcb26b
reintroduce convert in routes for group create
charliepark 36884e5
update max members value
charliepark 8e5e31c
Link to specific commit for line reference stability
charliepark e27d854
Add e2e tests
charliepark ff31223
Refresh of Affinity Groups table columns; use count in place of insta…
charliepark 4c7f8f3
update test
charliepark ee4e2f1
don't fetch affinity groups
david-crespo fb18dd3
members col -> instances, don't validate name uniqueness
david-crespo f635780
add delete and docs popover to group detail, use confirmDelete
david-crespo ff32750
help text on policy field and tip icon on policy columns
david-crespo 872feac
merge main and resolve conflicts with AffinityGroupPolicyBadge
charliepark 08b81e1
put back the line about policy in the popover
david-crespo ee98abb
remove title from icons in sidebar nav
david-crespo 5a58312
Refactoring form in add instance to A-A group modal
charliepark 99a2c78
type -> group type, remove description column
david-crespo 6f21f11
on second thought: make page title Affinity Groups
david-crespo 064879e
simplify form reset by unmounting, test that in e2e
david-crespo 16bb032
use handleSubmit higher so we don't have to type explicitly
david-crespo 37a6733
make enter submit add instance modal form
david-crespo 42f772e
link to instance settings rather than default tab
david-crespo 696484e
hopefully final policy help copy tweaks
david-crespo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| </> | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
david-crespo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.