From 23824251c582ff0321633490fce0f225b29aad57 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 30 Oct 2024 14:33:43 +0000 Subject: [PATCH] Instance action buttons (#2508) * Instance action buttons * Re-add serial console link * Remove test styles * Fix broken `stopInstance` test util * be fussy * Button tooltip position default bottom (also flips to top) * Use correct info icon size * Add tooltip padding from edge of viewport * Removed disabled cursor for disabled button * Add confirm start instance * fix e2es * Only "running" instances can be stopped * `disabled:cursor-default` on button * use helper for symmetry --------- Co-authored-by: David Crespo --- app/components/DocsPopover.tsx | 4 +- app/pages/project/instances/InstancesPage.tsx | 10 ++-- app/pages/project/instances/actions.tsx | 60 ++++++++++++------- .../instances/instance/InstancePage.tsx | 28 +++++++-- app/table/columns/action-col.tsx | 2 +- app/ui/lib/Button.tsx | 4 +- app/ui/lib/Tooltip.tsx | 2 + package-lock.json | 6 +- test/e2e/instance.e2e.ts | 2 + test/e2e/utils.ts | 3 +- 10 files changed, 78 insertions(+), 43 deletions(-) diff --git a/app/components/DocsPopover.tsx b/app/components/DocsPopover.tsx index b393f167c..76ddbcc79 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 { Info16Icon, 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 ( - + [ + ...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 b18886dd9..6b50afd03 100644 --- a/app/pages/project/instances/actions.tsx +++ b/app/pages/project/instances/actions.tsx @@ -14,7 +14,6 @@ import { HL } from '~/components/HL' import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' -import type { MakeActions } from '~/table/columns/action-col' import { pb } from '~/util/path-builder' import { fancifyStates } from './instance/tabs/common' @@ -31,9 +30,8 @@ type Options = { export const useMakeInstanceActions = ( { project }: { project: string }, options: Options = {} -): MakeActions => { +) => { 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(). // @@ -41,7 +39,7 @@ export const useMakeInstanceActions = ( // while the whole useMutation result object is not. The async ones are used // when we need to confirm because the confirm modals want that. const opts = { onSuccess: options.onSuccess } - const { mutate: startInstance } = useApiMutation('instanceStart', opts) + const { mutateAsync: startInstanceAsync } = useApiMutation('instanceStart', opts) const { mutateAsync: stopInstanceAsync } = useApiMutation('instanceStop', opts) const { mutate: rebootInstance } = useApiMutation('instanceReboot', opts) // delete has its own @@ -49,22 +47,32 @@ export const useMakeInstanceActions = ( onSuccess: options.onDelete, }) - return useCallback( - (instance) => { - const instanceSelector = { project, instance: instance.name } + const makeButtonActions = useCallback( + (instance: Instance) => { const instanceParams = { path: { instance: instance.name }, query: { project } } return [ { label: 'Start', onActivate() { - startInstance(instanceParams, { - onSuccess: () => addToast(<>Starting instance {instance.name}), // prettier-ignore - onError: (error) => - addToast({ - variant: 'error', - title: `Error starting instance '${instance.name}'`, - content: error.message, + confirmAction({ + actionType: 'primary', + doAction: () => + startInstanceAsync(instanceParams, { + onSuccess: () => addToast(<>Starting instance {instance.name}), // prettier-ignore + onError: (error) => + addToast({ + variant: 'error', + title: `Error starting instance '${instance.name}'`, + content: error.message, + }), }), + modalTitle: 'Confirm start instance', + modalContent: ( +

+ Are you sure you want to start {instance.name}? +

+ ), + errorTitle: `Error starting ${instance.name}`, }) }, disabled: !instanceCan.start(instance) && ( @@ -97,9 +105,20 @@ export const useMakeInstanceActions = ( }) }, disabled: !instanceCan.stop(instance) && ( - <>Only {fancifyStates(instanceCan.stop.states)} instances can be stopped + // don't list all the states, it's overwhelming + <>Only {fancifyStates(['running'])} instances can be stopped ), }, + ] + }, + [project, startInstanceAsync, stopInstanceAsync] + ) + + const makeMenuActions = useCallback( + (instance: Instance) => { + const instanceSelector = { project, instance: instance.name } + const instanceParams = { path: { instance: instance.name }, query: { project } } + return [ { label: 'Reboot', onActivate() { @@ -143,13 +162,8 @@ export const useMakeInstanceActions = ( }, ] }, - [ - project, - navigate, - deleteInstanceAsync, - rebootInstance, - startInstance, - stopInstanceAsync, - ] + [project, deleteInstanceAsync, navigate, rebootInstance] ) + + return { makeButtonActions, makeMenuActions } } diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 9736ad1be..f9c7fc30c 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 { makeButtonActions, makeMenuActions } = useMakeInstanceActions(instanceSelector, { onSuccess: refreshData, // go to project instances list since there's no more instance onDelete: () => { @@ -132,7 +134,7 @@ export function InstancePage() { { enabled: !!primaryVpcId } ) - const actions = useMemo( + const allMenuActions = useMemo( () => [ { label: 'Copy ID', @@ -140,9 +142,9 @@ export function InstancePage() { window.navigator.clipboard.writeText(instance.id || '') }, }, - ...makeActions(instance), + ...makeMenuActions(instance), ], - [instance, makeActions] + [instance, makeMenuActions] ) const memory = filesize(instance.memory, { output: 'object', base: 2 }) @@ -152,9 +154,23 @@ export function InstancePage() { }>{instance.name}
- - + +
+ {makeButtonActions(instance).map((action) => ( + + ))} +
+
diff --git a/app/table/columns/action-col.tsx b/app/table/columns/action-col.tsx index f18d16a04..a880245b4 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 diff --git a/app/ui/lib/Button.tsx b/app/ui/lib/Button.tsx index 1ced212a5..d77c893b6 100644 --- a/app/ui/lib/Button.tsx +++ b/app/ui/lib/Button.tsx @@ -35,7 +35,7 @@ export const buttonStyle = ({ variant = 'primary', }: ButtonStyleProps = {}) => { return cn( - 'ox-button elevation-1 rounded inline-flex items-center justify-center align-top disabled:cursor-not-allowed shrink-0', + 'ox-button elevation-1 rounded inline-flex items-center justify-center align-top disabled:cursor-default shrink-0', `btn-${variant}`, sizeStyle[size], variant === 'danger' @@ -87,7 +87,7 @@ export const Button = forwardRef( return ( } + with={} >