diff --git a/app/forms/instance-resize.tsx b/app/forms/instance-resize.tsx new file mode 100644 index 000000000..9d916a10e --- /dev/null +++ b/app/forms/instance-resize.tsx @@ -0,0 +1,75 @@ +/* + * 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-dom' +import * as R from 'remeda' + +import { + apiQueryClient, + useApiMutation, + useApiQueryClient, + usePrefetchedApiQuery, +} from '@oxide/api' + +import { NumberField } from '~/components/form/fields/NumberField' +import { SideModalForm } from '~/components/form/SideModalForm' +import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params' +import { addToast } from '~/stores/toast' +import { pb } from '~/util/path-builder' + +InstanceResizeForm.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, instance } = getInstanceSelector(params) + await apiQueryClient.prefetchQuery('instanceView', { + path: { instance }, + query: { project }, + }) + return null +} + +export function InstanceResizeForm() { + const { instance: instanceName, project } = useInstanceSelector() + const queryClient = useApiQueryClient() + const navigate = useNavigate() + + const { data: instance } = usePrefetchedApiQuery('instanceView', { + path: { instance: instanceName }, + query: { project }, + }) + + const instanceUpdate = useApiMutation('instanceUpdate', { + onSuccess(_updatedInstance) { + queryClient.invalidateQueries('instanceView') + navigate(pb.instance({ project, instance: instanceName })) + addToast({ title: 'Instance updated' }) + }, + }) + + const form = useForm({ defaultValues: R.pick(instance, ['ncpus', 'memory']) }) + + return ( + navigate(pb.instance({ project, instance: instanceName }))} + onSubmit={({ ncpus, memory }) => { + instanceUpdate.mutate({ + path: { instance: instanceName }, + query: { project }, + // very important to include the boot disk or it will be unset + body: { ncpus, memory, bootDisk: instance.bootDiskId }, + }) + }} + loading={instanceUpdate.isPending} + submitError={instanceUpdate.error} + > + + + + ) +} diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx index df251f3ab..79d7a7acc 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -118,10 +118,13 @@ export const useMakeInstanceActions = ( ), }, { - label: 'View serial console', + label: 'Resize', onActivate() { - navigate(pb.serialConsole(instanceSelector)) + navigate(pb.instanceResize(instanceSelector)) }, + disabled: !instanceCan.update(instance) && ( + <>Only {fancifyStates(instanceCan.update.states)} instances can be resized + ), }, { label: 'Delete', diff --git a/app/routes.tsx b/app/routes.tsx index 86b159f5f..2f1377547 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -23,6 +23,7 @@ import { import { CreateImageFromSnapshotSideModalForm } from './forms/image-from-snapshot' import { CreateImageSideModalForm } from './forms/image-upload' import { CreateInstanceForm } from './forms/instance-create' +import { InstanceResizeForm } from './forms/instance-resize' import { CreateIpPoolSideModalForm } from './forms/ip-pool-create' import { EditIpPoolSideModalForm } from './forms/ip-pool-edit' import { IpPoolAddRangeSideModalForm } from './forms/ip-pool-range-add' @@ -319,6 +320,17 @@ export const routes = createRoutesFromElements( loader={StorageTab.loader} handle={{ crumb: 'Storage' }} /> + + + + + } + loader={StorageTab.loader} + handle={{ crumb: 'Resize' }} + /> } diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 76e39846e..0f5789256 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -44,6 +44,7 @@ test('path builder', () => { "instanceConnect": "/projects/p/instances/i/connect", "instanceMetrics": "/projects/p/instances/i/metrics", "instanceNetworking": "/projects/p/instances/i/networking", + "instanceResize": "/projects/p/instances/i/resize", "instanceStorage": "/projects/p/instances/i/storage", "instances": "/projects/p/instances", "instancesNew": "/projects/p/instances-new", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 1709b19c5..b135a17dd 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -62,6 +62,7 @@ export const pb = { instanceStorage: (params: Instance) => `${instanceBase(params)}/storage`, instanceConnect: (params: Instance) => `${instanceBase(params)}/connect`, instanceNetworking: (params: Instance) => `${instanceBase(params)}/networking`, + instanceResize: (params: Instance) => `${instanceBase(params)}/resize`, serialConsole: (params: Instance) => `${instanceBase(params)}/serial-console`, disksNew: (params: Project) => `${projectBase(params)}/disks-new`,