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`,