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
2 changes: 1 addition & 1 deletion OMICRON_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d514878f417a94247791bd5564fbaafa9b4170a0
1f26c66921b9215bfe11d750514939bcdc11ae12
36 changes: 36 additions & 0 deletions app/api/__generated__/Api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/api/__generated__/OMICRON_VERSION

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions app/api/__generated__/msw-handlers.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions app/api/__generated__/validate.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions app/forms/floating-ip-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { getFloatingIpSelector, useFloatingIpSelector, useForm, useToast } from 'app/hooks'
import { pb } from 'app/util/path-builder'

EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { floatingIp, project } = getFloatingIpSelector(params)
await apiQueryClient.prefetchQuery('floatingIpView', {
path: { floatingIp },
query: { project },
})
return null
}

export function EditFloatingIpSideModalForm() {
const queryClient = useApiQueryClient()
const addToast = useToast()
const navigate = useNavigate()

const floatingIpSelector = useFloatingIpSelector()

const onDismiss = () => navigate(pb.floatingIps({ project: floatingIpSelector.project }))

const { data: floatingIp } = usePrefetchedApiQuery('floatingIpView', {
path: { floatingIp: floatingIpSelector.floatingIp },
query: { project: floatingIpSelector.project },
})

const editFloatingIp = useApiMutation('floatingIpUpdate', {
onSuccess(_floatingIp) {
queryClient.invalidateQueries('floatingIpList')
addToast({ content: 'Your floating IP has been updated' })
onDismiss()
},
})

const form = useForm({ defaultValues: floatingIp })

return (
<SideModalForm
id="edit-floating-ip-form"
form={form}
title="Edit floating IP"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
editFloatingIp.mutate({
path: { floatingIp: floatingIpSelector.floatingIp },
query: { project: floatingIpSelector.project },
body: { name, description },
})
}}
loading={editFloatingIp.isPending}
submitError={editFloatingIp.error}
submitLabel="Save changes"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
17 changes: 16 additions & 1 deletion app/pages/project/floating-ips/FloatingIpsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom'
import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
Expand Down Expand Up @@ -66,6 +66,7 @@ export function FloatingIpsPage() {
const { data: instances } = usePrefetchedApiQuery('instanceList', {
query: { project },
})
const navigate = useNavigate()
const getInstanceName = (instanceId: string) =>
instances.items.find((i) => i.id === instanceId)?.name

Expand Down Expand Up @@ -122,6 +123,20 @@ export function FloatingIpsPage() {
},
}
return [
{
label: 'Edit',
onActivate: () => {
apiQueryClient.setQueryData(
'floatingIpView',
{
path: { floatingIp: floatingIp.name },
query: { project },
},
floatingIp
)
navigate(pb.floatingIpEdit({ project, floatingIp: floatingIp.name }))
},
},
attachOrDetachAction,
{
label: 'Delete',
Expand Down
9 changes: 8 additions & 1 deletion app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RouterDataErrorBoundary } from './components/ErrorBoundary'
import { NotFound } from './components/ErrorPage'
import { CreateDiskSideModalForm } from './forms/disk-create'
import { CreateFloatingIpSideModalForm } from './forms/floating-ip-create'
import { EditFloatingIpSideModalForm } from './forms/floating-ip-edit'
import { CreateIdpSideModalForm } from './forms/idp/create'
import { EditIdpSideModalForm } from './forms/idp/edit'
import {
Expand Down Expand Up @@ -350,13 +351,19 @@ export const routes = createRoutesFromElements(
/>
</Route>

<Route loader={FloatingIpsPage.loader} element={<FloatingIpsPage />}>
<Route element={<FloatingIpsPage />} loader={FloatingIpsPage.loader}>
<Route path="floating-ips" handle={{ crumb: 'Floating IPs' }} element={null} />
<Route
path="floating-ips-new"
element={<CreateFloatingIpSideModalForm />}
handle={{ crumb: 'New Floating IP' }}
/>
<Route
path="floating-ips/:floatingIp/edit"
element={<EditFloatingIpSideModalForm />}
loader={EditFloatingIpSideModalForm.loader}
handle={{ crumb: 'Edit Floating IP' }}
/>
</Route>

<Route element={<DisksPage />} loader={DisksPage.loader}>
Expand Down
3 changes: 3 additions & 0 deletions app/util/path-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { pb } from './path-builder'

// params can be the same for all of them because they only use what they need
const params = {
floatingIp: 'f',
project: 'p',
instance: 'i',
vpc: 'v',
Expand All @@ -31,6 +32,8 @@ test('path builder', () => {
"diskInventory": "/system/inventory/disks",
"disks": "/projects/p/disks",
"disksNew": "/projects/p/disks-new",
"floatingIp": "/projects/p/floating-ips/f",
"floatingIpEdit": "/projects/p/floating-ips/f/edit",
"floatingIps": "/projects/p/floating-ips",
"floatingIpsNew": "/projects/p/floating-ips-new",
"instance": "/projects/p/instances/i",
Expand Down
3 changes: 3 additions & 0 deletions app/util/path-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Image = Required<PP.Image>
type Snapshot = Required<PP.Snapshot>
type SiloImage = Required<PP.SiloImage>
type IpPool = Required<PP.IpPool>
type FloatingIp = Required<PP.FloatingIp>

export const pb = {
projects: () => `/projects`,
Expand Down Expand Up @@ -68,6 +69,8 @@ export const pb = {
vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`,
floatingIps: (params: Project) => `${pb.project(params)}/floating-ips`,
floatingIpsNew: (params: Project) => `${pb.project(params)}/floating-ips-new`,
floatingIp: (params: FloatingIp) => `${pb.floatingIps(params)}/${params.floatingIp}`,
floatingIpEdit: (params: FloatingIp) => `${pb.floatingIp(params)}/edit`,

siloUtilization: () => '/utilization',
siloAccess: () => '/access',
Expand Down
18 changes: 15 additions & 3 deletions mock-api/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,20 @@ export const handlers = makeHandlers({
return paginated(query, ips)
},
floatingIpView: ({ path, query }) => lookup.floatingIp({ ...path, ...query }),
floatingIpUpdate: ({ path, query, body }) => {
const floatingIp = lookup.floatingIp({ ...path, ...query })
if (body.name) {
// only check for existing name if it's being changed
if (body.name !== floatingIp.name) {
errIfExists(db.floatingIps, { name: body.name, project_id: floatingIp.project_id })
}
floatingIp.name = body.name
}
if (body.description) {
floatingIp.description = body.description
}
return floatingIp
},
floatingIpDelete({ path, query }) {
const floatingIp = lookup.floatingIp({ ...path, ...query })
db.floatingIps = db.floatingIps.filter((i) => i.id !== floatingIp.id)
Expand Down Expand Up @@ -554,9 +568,7 @@ export const handlers = makeHandlers({
if (body.name) {
nic.name = body.name
}
if (typeof body.description === 'string') {
nic.description = body.description
}
nic.description = body.description || ''

if (typeof body.primary === 'boolean' && body.primary !== nic.primary) {
if (nic.primary) {
Expand Down
50 changes: 50 additions & 0 deletions test/e2e/floating-ip-update.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { clickRowAction, expect, expectRowVisible, expectVisible, test } from './utils'

const floatingIpsPage = '/projects/mock-project/floating-ips'
const originalName = 'cola-float'
const updatedName = 'updated-cola-float'
const updatedDescription = 'An updated description for this Floating IP'
const expectedFormElements = [
'role=heading[name*="Edit floating IP"]',
'role=textbox[name="Name"]',
'role=textbox[name="Description"]',
'role=button[name="Save changes"]',
]

test('can update a floating IP', async ({ page }) => {
await page.goto(floatingIpsPage)
await clickRowAction(page, 'cola-float', 'Edit')
await expectVisible(page, expectedFormElements)

await page.fill('input[name=name]', updatedName)
await page.getByRole('textbox', { name: 'Description' }).fill(updatedDescription)
await page.getByRole('button', { name: 'Save changes' }).click()
await expect(page).toHaveURL(floatingIpsPage)
await expectRowVisible(page.getByRole('table'), {
name: updatedName,
description: updatedDescription,
})
})

// Make sure that it still works even if the name doesn't change
test('can update *just* the floating IP description', async ({ page }) => {
// Go to the edit page for the original floating IP
await page.goto(`${floatingIpsPage}/${originalName}/edit`)
await expectVisible(page, expectedFormElements)

await page.getByRole('textbox', { name: 'Description' }).fill(updatedDescription)
await page.getByRole('button', { name: 'Save changes' }).click()
await expect(page).toHaveURL(floatingIpsPage)
await expectRowVisible(page.getByRole('table'), {
name: originalName,
description: updatedDescription,
})
})