From f66afff0fae47359d359c8297cb3b038d52e0287 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 9 Jan 2025 11:53:44 +0000 Subject: [PATCH 01/13] First pass instance auto-restart --- app/components/DocsPopover.tsx | 4 +- app/components/InstanceAutoRestartPopover.tsx | 158 ++++++++++++++++++ .../instances/instance/InstancePage.tsx | 14 +- .../instances/instance/tabs/SettingsTab.tsx | 147 ++++++++++++++++ app/routes.tsx | 2 + app/ui/lib/DropdownMenu.tsx | 2 +- app/ui/lib/Listbox.tsx | 2 +- app/ui/lib/SettingsGroup.tsx | 6 +- app/ui/styles/components/menu-button.css | 10 +- app/util/path-builder.spec.ts | 1 + app/util/path-builder.ts | 1 + mock-api/instance.ts | 1 + mock-api/msw/handlers.ts | 4 + tailwind.config.ts | 3 + 14 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 app/components/InstanceAutoRestartPopover.tsx create mode 100644 app/pages/project/instances/instance/tabs/SettingsTab.tsx diff --git a/app/components/DocsPopover.tsx b/app/components/DocsPopover.tsx index 82362feb14..2f00aff171 100644 --- a/app/components/DocsPopover.tsx +++ b/app/components/DocsPopover.tsx @@ -48,8 +48,8 @@ export const DocsPopover = ({ heading, icon, summary, links }: DocsPopoverProps)
diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx new file mode 100644 index 0000000000..2e7a0c61ea --- /dev/null +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -0,0 +1,158 @@ +import { CloseButton, Popover, PopoverButton, PopoverPanel } from '@headlessui/react' +import cn from 'classnames' +import { formatDistanceToNow } from 'date-fns' +import { useEffect, useState, type ReactNode } from 'react' +import { Link } from 'react-router' + +import { NextArrow12Icon, OpenLink12Icon } from '@oxide/design-system/icons/react' + +import type { InstanceAutoRestartPolicy } from '~/api' +import { HL } from '~/components/HL' +import { useInstanceSelector } from '~/hooks/use-params' +import { Badge } from '~/ui/lib/Badge' +import { Spinner } from '~/ui/lib/Spinner' +import { pb } from '~/util/path-builder' + +const helpText = { + enabled: ( + <>The control plane will attempt to automatically restart instance this instance. + ), + disabled: ( + <> + The control plane will not attempt to automatically restart it after entering the{' '} + failed state. + + ), + never: ( + <> + Instance auto-restart policy is set to never. The control plane will not attempt to + automatically restart it after entering the failed state. + + ), + starting: ( + <> + Instance auto-restart policy is queued to start. The control plane will begin the + restart process shortly. + + ), +} + +export const InstanceAutoRestartPopover = ({ + enabled, + policy, + cooldownExpiration, +}: { + enabled: boolean + policy: InstanceAutoRestartPolicy + cooldownExpiration: Date | undefined +}) => { + const instanceSelector = useInstanceSelector() + const [now, setNow] = useState(new Date()) + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + const isQueued = cooldownExpiration && new Date(cooldownExpiration) < now + + // todo: untangle this web + const helpTextState = isQueued + ? 'starting' + : policy === 'never' + ? 'never' + : enabled + ? 'enabled' + : ('disabled' as const) + + return ( + + + + + + + {enabled ? Enabled : Disabled} + + + + {policy ? ( + policy === 'never' ? ( + + never + + ) : ( + best effort + ) + ) : ( + Default + )} +
+ +
+
+
+ {cooldownExpiration && ( + + {isQueued ? ( + <> + Queued for restart… + + ) : ( +
+ Waiting{' '} + + ({formatDistanceToNow(cooldownExpiration)}) + +
+ )} +
+ )} +
+

{helpText[helpTextState]}

+ + + Learn about Instance Auto-Restart + + + +
+
+
+ ) +} + +const AutoRestartIcon12 = ({ className }: { className?: string }) => ( + + + +) + +const PopoverRow = ({ label, children }: { label: string; children: ReactNode }) => ( +
+
{label}
+
+ {children} +
+
+) diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 22acc7c97e..d882615bdb 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -29,6 +29,7 @@ import { import { ExternalIps } from '~/components/ExternalIps' import { NumberField } from '~/components/form/fields/NumberField' import { HL } from '~/components/HL' +import { InstanceAutoRestartPopover } from '~/components/InstanceAutoRestartPopover' import { InstanceDocsPopover } from '~/components/InstanceDocsPopover' import { MoreActionsMenu } from '~/components/MoreActionsMenu' import { RefreshButton } from '~/components/RefreshButton' @@ -168,6 +169,9 @@ export function InstancePage() { const memory = filesize(instance.memory, { output: 'object', base: 2 }) + // Document when this popover is showing + const hasAutoRestart = !!instance.autoRestartCooldownExpiration + return ( <> @@ -203,7 +207,7 @@ export function InstancePage() { {memory.unit} -
+
{polling && ( @@ -212,6 +216,13 @@ export function InstancePage() { )} + {hasAutoRestart && ( + + )}
@@ -241,6 +252,7 @@ export function InstancePage() { Metrics Networking Connect + Settings {resizeInstance && ( { + const timer = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + const { data: instance } = usePrefetchedApiQuery('instanceView', { + path: { instance: instanceSelector.instance }, + query: { project: instanceSelector.project }, + }) + + const instanceUpdate = useApiMutation('instanceUpdate', { + onSuccess() { + apiQueryClient.invalidateQueries('instanceView') + addToast({ content: 'Instance auto-restart policy updated' }) + }, + }) + + const form = useForm({ + defaultValues: { + autoRestartPolicy: instance.autoRestartPolicy, + }, + }) + const { isDirty } = form.formState + + const onSubmit = form.handleSubmit((values) => { + instanceUpdate.mutate({ + path: { instance: instanceSelector.instance }, + query: { project: instanceSelector.project }, + body: { + ncpus: instance.ncpus, + memory: instance.memory, + bootDisk: instance.bootDiskId, + autoRestartPolicy: values.autoRestartPolicy, + }, + }) + }) + + return ( +
+ + + Auto-restart +

The auto-restart policy configured for this instance

+
+ + + + {instance.autoRestartEnabled ? 'True' : 'False'}{' '} + {instance.autoRestartEnabled && !instance.autoRestartPolicy && ( + (Project default) + )} + + + {instance.autoRestartCooldownExpiration ? ( + <> + {format( + new Date(instance.autoRestartCooldownExpiration), + 'MMM d, yyyy HH:mm:ss zz' + )}{' '} + {new Date(instance.autoRestartCooldownExpiration) > now && ( + + ({formatDistanceToNow(new Date(instance.autoRestartCooldownExpiration))} + ) + + )} + + ) : ( + n/a + )} + + + +
+ +
+ +
+
+
+ ) +} + +const FormMeta = ({ + label, + helpText, + children, +}: { + label: string + helpText?: string + children: ReactNode +}) => { + const id = useId() + return ( +
+
+ + {label} + + {helpText && {helpText}} +
+ {children} +
+ ) +} diff --git a/app/routes.tsx b/app/routes.tsx index 780e603b5c..d71ffea989 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -62,6 +62,7 @@ import * as SerialConsole from './pages/project/instances/instance/SerialConsole import * as ConnectTab from './pages/project/instances/instance/tabs/ConnectTab' import * as MetricsTab from './pages/project/instances/instance/tabs/MetricsTab' import * as NetworkingTab from './pages/project/instances/instance/tabs/NetworkingTab' +import * as SettingsTab from './pages/project/instances/instance/tabs/SettingsTab' import * as StorageTab from './pages/project/instances/instance/tabs/StorageTab' import { InstancesPage } from './pages/project/instances/InstancesPage' import { SnapshotsPage } from './pages/project/snapshots/SnapshotsPage' @@ -295,6 +296,7 @@ export const routes = createRoutesFromElements( /> + diff --git a/app/ui/lib/DropdownMenu.tsx b/app/ui/lib/DropdownMenu.tsx index a6cf724d3c..f70954c30c 100644 --- a/app/ui/lib/DropdownMenu.tsx +++ b/app/ui/lib/DropdownMenu.tsx @@ -35,7 +35,7 @@ export function Content({ className, children, anchor = 'bottom end', gap }: Con anchor={anchor} // goofy gap because tailwind hates string interpolation className={cn( - 'DropdownMenuContent elevation-2', + 'dropdown-menu-content elevation-2', gap === 8 && `[--anchor-gap:8px]`, className )} diff --git a/app/ui/lib/Listbox.tsx b/app/ui/lib/Listbox.tsx index 6cd7af0106..c1617bb758 100644 --- a/app/ui/lib/Listbox.tsx +++ b/app/ui/lib/Listbox.tsx @@ -82,7 +82,7 @@ export const Listbox = ({ {({ open }) => ( <> {label && ( -
+
{ "floatingIpsNew": "/projects/p/floating-ips-new", "instance": "/projects/p/instances/i/storage", "instanceConnect": "/projects/p/instances/i/connect", + "instanceSettings": "/projects/p/instances/i/settings", "instanceMetrics": "/projects/p/instances/i/metrics", "instanceNetworking": "/projects/p/instances/i/networking", "instanceStorage": "/projects/p/instances/i/storage", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 5b5ab823f9..d227511a18 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -44,6 +44,7 @@ export const pb = { instanceConnect: (params: PP.Instance) => `${instanceBase(params)}/connect`, instanceNetworking: (params: PP.Instance) => `${instanceBase(params)}/networking`, serialConsole: (params: PP.Instance) => `${instanceBase(params)}/serial-console`, + instanceSettings: (params: PP.Instance) => `${instanceBase(params)}/settings`, disksNew: (params: PP.Project) => `${projectBase(params)}/disks-new`, disks: (params: PP.Project) => `${projectBase(params)}/disks`, diff --git a/mock-api/instance.ts b/mock-api/instance.ts index c274e1f16c..0f8a833e0b 100644 --- a/mock-api/instance.ts +++ b/mock-api/instance.ts @@ -42,6 +42,7 @@ const failedInstance: Json = { hostname: 'oxide.com', project_id: project.id, run_state: 'failed', + auto_restart_cooldown_expiration: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 mins in the future } const startingInstance: Json = { diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 376ad49714..6cca78755e 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -613,6 +613,10 @@ export const handlers = makeHandlers({ instance.boot_disk_id = undefined } + if (body.auto_restart_policy !== undefined) { + instance.auto_restart_policy = body.auto_restart_policy + } + // always present on the body, always set them instance.ncpus = body.ncpus instance.memory = body.memory diff --git a/tailwind.config.ts b/tailwind.config.ts index 4203962afd..e68c978a34 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -61,6 +61,9 @@ export default { transparent: 'transparent', current: 'currentColor', }, + animation: { + 'spin-slow': 'spin 5s linear infinite', + }, }, plugins: [ plugin(({ addVariant, addUtilities }) => { From 3be850038ff7b6ea97c27dd23c80521e56292163 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 9 Jan 2025 17:37:11 +0000 Subject: [PATCH 02/13] Add comma to settings help text Co-authored-by: Eliza Weisman --- app/pages/project/instances/instance/tabs/SettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/project/instances/instance/tabs/SettingsTab.tsx b/app/pages/project/instances/instance/tabs/SettingsTab.tsx index f77b70c1b3..cf354b3e02 100644 --- a/app/pages/project/instances/instance/tabs/SettingsTab.tsx +++ b/app/pages/project/instances/instance/tabs/SettingsTab.tsx @@ -75,7 +75,7 @@ export function Component() { control={form.control} name="autoRestartPolicy" label="Policy" - description="If unconfigured this instance uses the default auto-restart policy, which may or may not allow it to be restarted." + description="If unconfigured, this instance uses the default auto-restart policy, which may or may not allow it to be restarted." placeholder="Default" items={[ { value: '', label: 'None (Default)' }, From 8d40e2fd2cac0fbe6520b2ab3399f443918dbcc2 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 11:48:36 +0000 Subject: [PATCH 03/13] Add cooldown and add copy tweaks --- app/components/InstanceAutoRestartPopover.tsx | 4 +-- .../instances/instance/tabs/SettingsTab.tsx | 25 +++++++++++++++++-- mock-api/instance.ts | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index 2e7a0c61ea..68d79b5491 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -103,7 +103,7 @@ export const InstanceAutoRestartPopover = ({ {cooldownExpiration && ( - + {isQueued ? ( <> Queued for restart… @@ -151,7 +151,7 @@ const AutoRestartIcon12 = ({ className }: { className?: string }) => ( const PopoverRow = ({ label, children }: { label: string; children: ReactNode }) => (
{label}
-
+
{children}
diff --git a/app/pages/project/instances/instance/tabs/SettingsTab.tsx b/app/pages/project/instances/instance/tabs/SettingsTab.tsx index cf354b3e02..76d5248f04 100644 --- a/app/pages/project/instances/instance/tabs/SettingsTab.tsx +++ b/app/pages/project/instances/instance/tabs/SettingsTab.tsx @@ -85,13 +85,19 @@ export function Component() { required className="max-w-none" /> - + {instance.autoRestartEnabled ? 'True' : 'False'}{' '} {instance.autoRestartEnabled && !instance.autoRestartPolicy && ( (Project default) )} - + {instance.autoRestartCooldownExpiration ? ( <> {format( @@ -109,6 +115,21 @@ export function Component() { n/a )} + + {instance.timeLastAutoRestarted ? ( + <> + {format( + new Date(instance.timeLastAutoRestarted), + 'MMM d, yyyy HH:mm:ss zz' + )} + + ) : ( + n/a + )} +
diff --git a/mock-api/instance.ts b/mock-api/instance.ts index 0f8a833e0b..3f93b6a4f5 100644 --- a/mock-api/instance.ts +++ b/mock-api/instance.ts @@ -43,6 +43,7 @@ const failedInstance: Json = { project_id: project.id, run_state: 'failed', auto_restart_cooldown_expiration: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 mins in the future + time_last_auto_restarted: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // one month ago } const startingInstance: Json = { From bf37c96d0c2d0efb850e321bfa90fcd13f26ea9b Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 12:02:36 +0000 Subject: [PATCH 04/13] Add license --- app/components/InstanceAutoRestartPopover.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index 68d79b5491..7e9ee94e4d 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -1,3 +1,10 @@ +/* + * 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 { CloseButton, Popover, PopoverButton, PopoverPanel } from '@headlessui/react' import cn from 'classnames' import { formatDistanceToNow } from 'date-fns' From d81c471eed79b6ae9c7684f13e1586dd84e47405 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 12:03:41 +0000 Subject: [PATCH 05/13] Change none to default in listbox text --- app/pages/project/instances/instance/tabs/SettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/project/instances/instance/tabs/SettingsTab.tsx b/app/pages/project/instances/instance/tabs/SettingsTab.tsx index 76d5248f04..46bc43cd01 100644 --- a/app/pages/project/instances/instance/tabs/SettingsTab.tsx +++ b/app/pages/project/instances/instance/tabs/SettingsTab.tsx @@ -78,7 +78,7 @@ export function Component() { description="If unconfigured, this instance uses the default auto-restart policy, which may or may not allow it to be restarted." placeholder="Default" items={[ - { value: '', label: 'None (Default)' }, + { value: '', label: 'Default' }, { value: 'never', label: 'Never' }, { value: 'best_effort', label: 'Best effort' }, ]} From 06400f890da98c1f5d44e94c5081e3ee26a46fef Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 12:10:39 +0000 Subject: [PATCH 06/13] Update mock handler to match omicron --- mock-api/msw/handlers.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 6cca78755e..36e0e7a263 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -584,7 +584,23 @@ export const handlers = makeHandlers({ return json(newInstance, { status: 201 }) }, - instanceView: ({ path, query }) => lookup.instance({ ...path, ...query }), + instanceView: ({ path, query }) => { + const instance = lookup.instance({ ...path, ...query }) + + // if empty uses default auto-restart behaviour + // if set, is based off of the policy + // https://github.com/oxidecomputer/omicron/blob/f63ed095e744fb8d2383fda6799eb0b2d6dfbd3c/nexus/db-queries/src/db/datastore/instance.rs#L228C26-L239 + if (instance.auto_restart_policy === 'never') { + instance.auto_restart_enabled = false + } else if ( + instance.auto_restart_policy === 'best_effort' || + !instance.auto_restart_policy // included for posterity but this has to be set in the mock data anyway + ) { + instance.auto_restart_enabled = true + } + + return instance + }, instanceUpdate({ path, query, body }) { const instance = lookup.instance({ ...path, ...query }) From a11042a7417a933fe8b9c7edb9020d0ad2c60ee1 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 12:56:59 +0000 Subject: [PATCH 07/13] Use `useInterval` hook --- app/components/InstanceAutoRestartPopover.tsx | 8 +++----- app/pages/project/instances/instance/tabs/SettingsTab.tsx | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index 7e9ee94e4d..57e5d16a1f 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -8,7 +8,7 @@ import { CloseButton, Popover, PopoverButton, PopoverPanel } from '@headlessui/react' import cn from 'classnames' import { formatDistanceToNow } from 'date-fns' -import { useEffect, useState, type ReactNode } from 'react' +import { useState, type ReactNode } from 'react' import { Link } from 'react-router' import { NextArrow12Icon, OpenLink12Icon } from '@oxide/design-system/icons/react' @@ -18,6 +18,7 @@ import { HL } from '~/components/HL' import { useInstanceSelector } from '~/hooks/use-params' import { Badge } from '~/ui/lib/Badge' import { Spinner } from '~/ui/lib/Spinner' +import { useInterval } from '~/ui/lib/use-interval' import { pb } from '~/util/path-builder' const helpText = { @@ -56,10 +57,7 @@ export const InstanceAutoRestartPopover = ({ const instanceSelector = useInstanceSelector() const [now, setNow] = useState(new Date()) - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000) - return () => clearInterval(timer) - }, []) + useInterval({ fn: () => setNow(new Date()), delay: 1000 }) const isQueued = cooldownExpiration && new Date(cooldownExpiration) < now diff --git a/app/pages/project/instances/instance/tabs/SettingsTab.tsx b/app/pages/project/instances/instance/tabs/SettingsTab.tsx index 46bc43cd01..90a89dbe54 100644 --- a/app/pages/project/instances/instance/tabs/SettingsTab.tsx +++ b/app/pages/project/instances/instance/tabs/SettingsTab.tsx @@ -7,7 +7,7 @@ */ import { format, formatDistanceToNow } from 'date-fns' -import { useEffect, useId, useState, type ReactNode } from 'react' +import { useId, useState, type ReactNode } from 'react' import { useForm } from 'react-hook-form' import { apiQueryClient, useApiMutation, usePrefetchedApiQuery } from '~/api' @@ -18,6 +18,7 @@ import { Button } from '~/ui/lib/Button' import { FieldLabel } from '~/ui/lib/FieldLabel' import { LearnMore, SettingsGroup } from '~/ui/lib/SettingsGroup' import { TipIcon } from '~/ui/lib/TipIcon' +import { useInterval } from '~/ui/lib/use-interval' import { links } from '~/util/links' Component.displayName = 'SettingsTab' @@ -26,10 +27,7 @@ export function Component() { const [now, setNow] = useState(new Date()) - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000) - return () => clearInterval(timer) - }, []) + useInterval({ fn: () => setNow(new Date()), delay: 1000 }) const { data: instance } = usePrefetchedApiQuery('instanceView', { path: { instance: instanceSelector.instance }, From c03afd2cce5bffd995aa1a69102636aea2289f68 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 14:04:59 +0000 Subject: [PATCH 08/13] Use `design-system` icon --- app/components/InstanceAutoRestartPopover.tsx | 24 +-- package-lock.json | 152 +----------------- package.json | 2 +- 3 files changed, 12 insertions(+), 166 deletions(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index 57e5d16a1f..6af803984f 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -11,7 +11,11 @@ import { formatDistanceToNow } from 'date-fns' import { useState, type ReactNode } from 'react' import { Link } from 'react-router' -import { NextArrow12Icon, OpenLink12Icon } from '@oxide/design-system/icons/react' +import { + AutoRestart12Icon, + NextArrow12Icon, + OpenLink12Icon, +} from '@oxide/design-system/icons/react' import type { InstanceAutoRestartPolicy } from '~/api' import { HL } from '~/components/HL' @@ -73,7 +77,7 @@ export const InstanceAutoRestartPopover = ({ return ( - @@ -137,22 +141,6 @@ export const InstanceAutoRestartPopover = ({ ) } -const AutoRestartIcon12 = ({ className }: { className?: string }) => ( - - - -) - const PopoverRow = ({ label, children }: { label: string; children: ReactNode }) => (
{label}
diff --git a/package-lock.json b/package-lock.json index 5159e7bc15..a7715e75a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@floating-ui/react": "^0.26.23", "@headlessui/react": "^2.2.0", - "@oxide/design-system": "^1.7.3", + "@oxide/design-system": "^3.0.1--canary.0de862a.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-focus-guards": "1.0.1", @@ -1547,9 +1547,9 @@ "license": "MIT" }, "node_modules/@oxide/design-system": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-1.8.3.tgz", - "integrity": "sha512-N14ayGhpw2W/hJgqH8QTKns/HWPOJ3A/ZkfpfwEvx8MtjUEgkl8gTfNciukwe5hR4ZU0oFxN7cEurjffYeJCgg==", + "version": "3.0.1--canary.0de862a.0", + "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-3.0.1--canary.0de862a.0.tgz", + "integrity": "sha512-d2LLzwJt3s7IIS6+ghi/7NfRqhGGS+udAuPsfHABM9wJV1LIl8kISCy9jYp2gEvZMCsfVJl2zJO2F9XfKkJhOQ==", "license": "MPL 2.0", "dependencies": { "@floating-ui/react": "^0.25.1", @@ -1562,7 +1562,6 @@ "peerDependencies": { "@asciidoctor/core": "^3.0.0", "@oxide/react-asciidoc": "^1.0.0", - "@remix-run/react": "2.13.1", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -4064,96 +4063,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@remix-run/react": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.13.1.tgz", - "integrity": "sha512-kZevCoKMz0ZDOOzTnG95yfM7M9ju38FkWNY1wtxCy+NnUJYrmTerGQtiBsJgMzYD6i29+w4EwoQsdqys7DmMSg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@remix-run/router": "1.20.0", - "@remix-run/server-runtime": "2.13.1", - "react-router": "6.27.0", - "react-router-dom": "6.27.0", - "turbo-stream": "2.4.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/react/node_modules/react-router": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", - "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@remix-run/router": "1.20.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@remix-run/router": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", - "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@remix-run/server-runtime": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.13.1.tgz", - "integrity": "sha512-2DfBPRcHKVzE4bCNsNkKB50BhCCKF73x+jiS836OyxSIAL+x0tguV2AEjmGXefEXc5AGGzoxkus0AUUEYa29Vg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@remix-run/router": "1.20.0", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3", - "turbo-stream": "2.4.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/server-runtime/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -5649,13 +5558,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@web3-storage/multipart-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", - "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==", - "license": "(Apache-2.0 AND MIT)", - "peer": true - }, "node_modules/@xterm/addon-fit": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", @@ -12475,40 +12377,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", - "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@remix-run/router": "1.20.0", - "react-router": "6.27.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-router-dom/node_modules/react-router": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", - "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@remix-run/router": "1.20.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, "node_modules/react-router/node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -13414,16 +13282,6 @@ "react": ">=16.8.0" } }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -14436,7 +14294,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 4dc0dbeab8..531ce0e52c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "@floating-ui/react": "^0.26.23", "@headlessui/react": "^2.2.0", - "@oxide/design-system": "^1.7.3", + "@oxide/design-system": "^3.0.1--canary.0de862a.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-focus-guards": "1.0.1", From 3212357d41013b1a45bfdffa10935db7149252fe Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 14:28:31 +0000 Subject: [PATCH 09/13] Text tweak --- app/components/InstanceAutoRestartPopover.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index 6af803984f..ff138536d1 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -27,7 +27,10 @@ import { pb } from '~/util/path-builder' const helpText = { enabled: ( - <>The control plane will attempt to automatically restart instance this instance. + <> + The control plane will attempt to automatically restart instance this instance after + entering the failed state. + ), disabled: ( <> From 7ada68ea3e020ac38fb3e51e97d4ae77696c1ee1 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 14:39:08 +0000 Subject: [PATCH 10/13] Simplify `helpTextState` --- app/components/InstanceAutoRestartPopover.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index ff138536d1..a6330e5aa2 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -68,14 +68,10 @@ export const InstanceAutoRestartPopover = ({ const isQueued = cooldownExpiration && new Date(cooldownExpiration) < now - // todo: untangle this web - const helpTextState = isQueued - ? 'starting' - : policy === 'never' - ? 'never' - : enabled - ? 'enabled' - : ('disabled' as const) + let helpTextState: keyof typeof helpText = 'disabled' + if (isQueued) helpTextState = 'starting' // Expiration is in the past and queued for restart + if (policy === 'never') helpTextState = 'never' // Will never auto-restart + if (enabled) helpTextState = 'enabled' // Restart enabled and cooldown as not expired return ( From a5997452ccda637549ade8c1340eea5cbd46a08d Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 14:39:16 +0000 Subject: [PATCH 11/13] `InstanceAutoRestartPolicy` can be undefined --- app/components/InstanceAutoRestartPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/InstanceAutoRestartPopover.tsx b/app/components/InstanceAutoRestartPopover.tsx index a6330e5aa2..daca276832 100644 --- a/app/components/InstanceAutoRestartPopover.tsx +++ b/app/components/InstanceAutoRestartPopover.tsx @@ -58,7 +58,7 @@ export const InstanceAutoRestartPopover = ({ cooldownExpiration, }: { enabled: boolean - policy: InstanceAutoRestartPolicy + policy?: InstanceAutoRestartPolicy cooldownExpiration: Date | undefined }) => { const instanceSelector = useInstanceSelector() From 467b58e874a572d48add1ace06da232e4d1d302b Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 14 Jan 2025 14:50:00 +0000 Subject: [PATCH 12/13] Vitest fixes --- .../__snapshots__/path-builder.spec.ts.snap | 22 +++++++++++++++++++ app/util/path-builder.spec.ts | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 7142b88c84..0682007ae5 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -171,6 +171,28 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/instances/i/networking", }, ], + "instanceSettings (/projects/p/instances/i/settings)": [ + { + "label": "Projects", + "path": "/projects", + }, + { + "label": "p", + "path": "/projects/p/instances", + }, + { + "label": "Instances", + "path": "/projects/p/instances", + }, + { + "label": "i", + "path": "/projects/p/instances/i/storage", + }, + { + "label": "Settings", + "path": "/projects/p/instances/i/settings", + }, + ], "instanceStorage (/projects/p/instances/i/storage)": [ { "label": "Projects", diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 3a24da37e2..f094d10312 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -48,9 +48,9 @@ test('path builder', () => { "floatingIpsNew": "/projects/p/floating-ips-new", "instance": "/projects/p/instances/i/storage", "instanceConnect": "/projects/p/instances/i/connect", - "instanceSettings": "/projects/p/instances/i/settings", "instanceMetrics": "/projects/p/instances/i/metrics", "instanceNetworking": "/projects/p/instances/i/networking", + "instanceSettings": "/projects/p/instances/i/settings", "instanceStorage": "/projects/p/instances/i/storage", "instances": "/projects/p/instances", "instancesNew": "/projects/p/instances-new", From ebe8dc08906128eb43b64993abae05acf798027f Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 21 Jan 2025 10:42:52 +0000 Subject: [PATCH 13/13] Upgrade `@oxide/design-system` --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1726a1239d..a56de5dfb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@floating-ui/react": "^0.26.23", "@headlessui/react": "^2.2.0", - "@oxide/design-system": "^3.0.1--canary.0de862a.0", + "@oxide/design-system": "^2.1.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-focus-guards": "1.0.1", @@ -1547,9 +1547,9 @@ "license": "MIT" }, "node_modules/@oxide/design-system": { - "version": "3.0.1--canary.0de862a.0", - "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-3.0.1--canary.0de862a.0.tgz", - "integrity": "sha512-d2LLzwJt3s7IIS6+ghi/7NfRqhGGS+udAuPsfHABM9wJV1LIl8kISCy9jYp2gEvZMCsfVJl2zJO2F9XfKkJhOQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-2.1.0.tgz", + "integrity": "sha512-aQpD+vZQIgkBzi9jcX35m1z1milAf4PzewVH9FlMMGniwUNCXw6G6KT8t3Unnwy3egUQ/qvhdQqm9VSdoDQwsA==", "license": "MPL 2.0", "dependencies": { "@floating-ui/react": "^0.25.1", diff --git a/package.json b/package.json index 4d7d10d423..0544ff0e31 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "@floating-ui/react": "^0.26.23", "@headlessui/react": "^2.2.0", - "@oxide/design-system": "^3.0.1--canary.0de862a.0", + "@oxide/design-system": "^2.1.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-focus-guards": "1.0.1",