From cc4cb20e234a6960701ac8332d007f40f544848e Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 17 Oct 2024 13:42:03 +0100 Subject: [PATCH 01/14] Instance action buttons --- app/components/DocsPopover.tsx | 4 +- app/pages/project/instances/InstancesPage.tsx | 2 +- app/pages/project/instances/actions.tsx | 47 +++++++++---------- .../instances/instance/InstancePage.tsx | 29 +++++++++--- 4 files changed, 49 insertions(+), 33 deletions(-) diff --git a/app/components/DocsPopover.tsx b/app/components/DocsPopover.tsx index b393f167c5..a4f5544326 100644 --- a/app/components/DocsPopover.tsx +++ b/app/components/DocsPopover.tsx @@ -9,7 +9,7 @@ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' import cn from 'classnames' -import { OpenLink12Icon, Question12Icon } from '@oxide/design-system/icons/react' +import { Info12Icon, OpenLink12Icon } from '@oxide/design-system/icons/react' import { buttonStyle } from '~/ui/lib/Button' @@ -45,7 +45,7 @@ export const DocsPopover = ({ heading, icon, summary, links }: DocsPopoverProps) return ( - + => { - const navigate = useNavigate() - +) => { // if you also pass onSuccess to mutate(), this one is not overridden — this // one runs first, then the one passed to mutate(). // @@ -49,9 +44,8 @@ export const useMakeInstanceActions = ( onSuccess: options.onDelete, }) - return useCallback( - (instance) => { - const instanceSelector = { project, instance: instance.name } + const makeActions = useCallback( + (instance: Instance) => { const instanceParams = { path: { instance: instance.name }, query: { project } } return [ { @@ -117,12 +111,6 @@ export const useMakeInstanceActions = ( <>Only {fancifyStates(instanceCan.reboot.states)} instances can be rebooted ), }, - { - label: 'View serial console', - onActivate() { - navigate(pb.serialConsole(instanceSelector)) - }, - }, { label: 'Delete', onActivate: confirmDelete({ @@ -142,13 +130,24 @@ export const useMakeInstanceActions = ( }, ] }, - [ - project, - navigate, - deleteInstanceAsync, - rebootInstance, - startInstance, - stopInstanceAsync, - ] + [project, deleteInstanceAsync, rebootInstance, startInstance, stopInstanceAsync] ) + + const useInstanceActions = (instance: Instance) => { + const allActions = useMemo(() => makeActions(instance), [instance]) + + const buttonActions = useMemo( + () => allActions.filter((a) => a.label === 'Start' || a.label === 'Stop'), + [allActions] + ) + + const menuActions = useMemo( + () => allActions.filter((a) => a.label !== 'Start' && a.label !== 'Stop'), + [allActions] + ) + + return { allActions, buttonActions, menuActions } + } + + return { useInstanceActions, makeActions } } diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 9736ad1bef..aeb7e941af 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -26,6 +26,7 @@ import { RouteTabs, Tab } from '~/components/RouteTabs' import { InstanceStateBadge } from '~/components/StateBadge' import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params' import { EmptyCell } from '~/table/cells/EmptyCell' +import { Button } from '~/ui/lib/Button' import { DateTime } from '~/ui/lib/DateTime' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' @@ -92,7 +93,8 @@ export function InstancePage() { const instanceSelector = useInstanceSelector() const navigate = useNavigate() - const makeActions = useMakeInstanceActions(instanceSelector, { + + const { useInstanceActions } = useMakeInstanceActions(instanceSelector, { onSuccess: refreshData, // go to project instances list since there's no more instance onDelete: () => { @@ -132,7 +134,8 @@ export function InstancePage() { { enabled: !!primaryVpcId } ) - const actions = useMemo( + const { buttonActions, menuActions } = useInstanceActions(instance) + const allMenuActions = useMemo( () => [ { label: 'Copy ID', @@ -140,9 +143,9 @@ export function InstancePage() { window.navigator.clipboard.writeText(instance.id || '') }, }, - ...makeActions(instance), + ...menuActions, ], - [instance, makeActions] + [instance.id, menuActions] ) const memory = filesize(instance.memory, { output: 'object', base: 2 }) @@ -152,9 +155,23 @@ export function InstancePage() { }>{instance.name}
- - + +
+ {buttonActions.map((action) => ( + + ))} +
+
From d746d46dc1eb8e87da9de91b44eb58977dfed29e Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 17 Oct 2024 13:49:06 +0100 Subject: [PATCH 02/14] Re-add serial console link --- app/pages/project/instances/actions.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx index 823ccd53a8..db6c27e1bc 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { useCallback, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' import { instanceCan, useApiMutation, type Instance } from '@oxide/api' @@ -13,6 +14,7 @@ import { HL } from '~/components/HL' import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' +import { pb } from '~/util/path-builder' import { fancifyStates } from './instance/tabs/common' @@ -29,6 +31,7 @@ export const useMakeInstanceActions = ( { project }: { project: string }, options: Options = {} ) => { + const navigate = useNavigate() // if you also pass onSuccess to mutate(), this one is not overridden — this // one runs first, then the one passed to mutate(). // @@ -46,6 +49,7 @@ export const useMakeInstanceActions = ( const makeActions = useCallback( (instance: Instance) => { + const instanceSelector = { project, instance: instance.name } const instanceParams = { path: { instance: instance.name }, query: { project } } return [ { @@ -111,6 +115,12 @@ export const useMakeInstanceActions = ( <>Only {fancifyStates(instanceCan.reboot.states)} instances can be rebooted ), }, + { + label: 'View serial console', + onActivate() { + navigate(pb.serialConsole(instanceSelector)) + }, + }, { label: 'Delete', onActivate: confirmDelete({ @@ -130,7 +140,14 @@ export const useMakeInstanceActions = ( }, ] }, - [project, deleteInstanceAsync, rebootInstance, startInstance, stopInstanceAsync] + [ + project, + deleteInstanceAsync, + navigate, + rebootInstance, + startInstance, + stopInstanceAsync, + ] ) const useInstanceActions = (instance: Instance) => { From d02bf644131a1157f3e60c507c4c1aedf028af7e Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 17 Oct 2024 13:50:23 +0100 Subject: [PATCH 03/14] Remove test styles --- app/pages/project/instances/instance/InstancePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index aeb7e941af..7bfb2c2ded 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -165,7 +165,6 @@ export function InstancePage() { size="sm" onClick={action.onActivate} disabled={!!action.disabled} - className="text-sm bg-gray-200 rounded px-2 py-1" > {action.label} From 22d584f449d33ca6347b2f356a3da6b1752e9079 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 17 Oct 2024 15:16:48 +0100 Subject: [PATCH 04/14] Fix broken `stopInstance` test util --- test/e2e/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index e74c39e0ae..963fb8ebf9 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -107,8 +107,7 @@ export async function expectRowVisible( } export async function stopInstance(page: Page) { - await page.getByRole('button', { name: 'Instance actions' }).click() - await page.getByRole('menuitem', { name: 'Stop' }).click() + await page.getByRole('button', { name: 'Stop' }).click() await page.getByRole('button', { name: 'Confirm' }).click() await closeToast(page) // don't need to manually refresh because of polling From 05725681d74c657236d3be60f81c95d110b339d0 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 17 Oct 2024 12:13:36 -0500 Subject: [PATCH 05/14] be fussy --- app/pages/project/instances/InstancesPage.tsx | 10 +++-- app/pages/project/instances/actions.tsx | 42 +++++++------------ .../instances/instance/InstancePage.tsx | 10 ++--- app/table/columns/action-col.tsx | 2 +- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index 94f940836e..97de119b4f 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -64,8 +64,7 @@ const POLL_INTERVAL_SLOW = 60 * sec export function InstancesPage() { const { project } = useProjectSelector() - - const { makeActions } = useMakeInstanceActions( + const { makeButtonActions, makeMenuActions } = useMakeInstanceActions( { project }, { onSuccess: refetchInstances, onDelete: refetchInstances } ) @@ -182,9 +181,12 @@ export function InstancesPage() { } ), colHelper.accessor('timeCreated', Columns.timeCreated), - getActionsCol(makeActions), + getActionsCol((instance: Instance) => [ + ...makeButtonActions(instance), + ...makeMenuActions(instance), + ]), ], - [project, makeActions] + [project, makeButtonActions, makeMenuActions] ) if (!instances) return null diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx index db6c27e1bc..2fca5344a0 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { instanceCan, useApiMutation, type Instance } from '@oxide/api' @@ -47,9 +47,8 @@ export const useMakeInstanceActions = ( onSuccess: options.onDelete, }) - const makeActions = useCallback( + const makeButtonActions = useCallback( (instance: Instance) => { - const instanceSelector = { project, instance: instance.name } const instanceParams = { path: { instance: instance.name }, query: { project } } return [ { @@ -98,6 +97,16 @@ export const useMakeInstanceActions = ( <>Only {fancifyStates(instanceCan.stop.states)} instances can be stopped ), }, + ] + }, + [project, startInstance, stopInstanceAsync] + ) + + const makeMenuActions = useCallback( + (instance: Instance) => { + const instanceSelector = { project, instance: instance.name } + const instanceParams = { path: { instance: instance.name }, query: { project } } + return [ { label: 'Reboot', onActivate() { @@ -140,31 +149,8 @@ export const useMakeInstanceActions = ( }, ] }, - [ - project, - deleteInstanceAsync, - navigate, - rebootInstance, - startInstance, - stopInstanceAsync, - ] + [project, deleteInstanceAsync, navigate, rebootInstance] ) - const useInstanceActions = (instance: Instance) => { - const allActions = useMemo(() => makeActions(instance), [instance]) - - const buttonActions = useMemo( - () => allActions.filter((a) => a.label === 'Start' || a.label === 'Stop'), - [allActions] - ) - - const menuActions = useMemo( - () => allActions.filter((a) => a.label !== 'Start' && a.label !== 'Stop'), - [allActions] - ) - - return { allActions, buttonActions, menuActions } - } - - return { useInstanceActions, makeActions } + return { makeButtonActions, makeMenuActions } } diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 7bfb2c2ded..f9c7fc30cc 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -94,7 +94,7 @@ export function InstancePage() { const navigate = useNavigate() - const { useInstanceActions } = useMakeInstanceActions(instanceSelector, { + const { makeButtonActions, makeMenuActions } = useMakeInstanceActions(instanceSelector, { onSuccess: refreshData, // go to project instances list since there's no more instance onDelete: () => { @@ -134,7 +134,6 @@ export function InstancePage() { { enabled: !!primaryVpcId } ) - const { buttonActions, menuActions } = useInstanceActions(instance) const allMenuActions = useMemo( () => [ { @@ -143,9 +142,9 @@ export function InstancePage() { window.navigator.clipboard.writeText(instance.id || '') }, }, - ...menuActions, + ...makeMenuActions(instance), ], - [instance.id, menuActions] + [instance, makeMenuActions] ) const memory = filesize(instance.memory, { output: 'object', base: 2 }) @@ -158,13 +157,14 @@ export function InstancePage() {
- {buttonActions.map((action) => ( + {makeButtonActions(instance).map((action) => ( diff --git a/app/table/columns/action-col.tsx b/app/table/columns/action-col.tsx index f18d16a04a..a880245b45 100644 --- a/app/table/columns/action-col.tsx +++ b/app/table/columns/action-col.tsx @@ -16,7 +16,7 @@ import { Tooltip } from '~/ui/lib/Tooltip' import { Wrap } from '~/ui/util/wrap' import { kebabCase } from '~/util/str' -export type MakeActions = (item: Item) => Array +type MakeActions = (item: Item) => Array export type MenuAction = { label: string From 0947094fe75b1a574b8aaa93d35a98883b406d54 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 29 Oct 2024 15:40:23 +0000 Subject: [PATCH 06/14] Button tooltip position default bottom (also flips to top) --- app/ui/lib/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/lib/Button.tsx b/app/ui/lib/Button.tsx index 1ced212a58..eeba09391d 100644 --- a/app/ui/lib/Button.tsx +++ b/app/ui/lib/Button.tsx @@ -87,7 +87,7 @@ export const Button = forwardRef( return ( } + with={} >