Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/components/DocsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -45,7 +45,7 @@ export const DocsPopover = ({ heading, icon, summary, links }: DocsPopoverProps)
return (
<Popover>
<PopoverButton className={cn(buttonStyle({ size: 'sm', variant: 'ghost' }), 'w-8')}>
<Question12Icon aria-label="Links to docs" className="shrink-0" />
<Info16Icon aria-label="Links to docs" className="shrink-0" />
</PopoverButton>
<PopoverPanel
// DocsPopoverPanel needed for enter animation
Expand Down
10 changes: 6 additions & 4 deletions app/pages/project/instances/InstancesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
)
Expand Down Expand Up @@ -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
Expand Down
60 changes: 37 additions & 23 deletions app/pages/project/instances/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -31,40 +30,49 @@ type Options = {
export const useMakeInstanceActions = (
{ project }: { project: string },
options: Options = {}
): MakeActions<Instance> => {
) => {
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().
//
// We pull out the mutate functions because they are referentially stable,
// 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
const { mutateAsync: deleteInstanceAsync } = useApiMutation('instanceDelete', {
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 <HL>{instance.name}</HL></>), // 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 <HL>{instance.name}</HL></>), // prettier-ignore
onError: (error) =>
addToast({
variant: 'error',
title: `Error starting instance '${instance.name}'`,
content: error.message,
}),
}),
modalTitle: 'Confirm start instance',
modalContent: (
<p>
Are you sure you want to start <HL>{instance.name}</HL>?
</p>
),
errorTitle: `Error starting ${instance.name}`,
})
},
disabled: !instanceCan.start(instance) && (
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -143,13 +162,8 @@ export const useMakeInstanceActions = (
},
]
},
[
project,
navigate,
deleteInstanceAsync,
rebootInstance,
startInstance,
stopInstanceAsync,
]
[project, deleteInstanceAsync, navigate, rebootInstance]
)

return { makeButtonActions, makeMenuActions }
}
28 changes: 22 additions & 6 deletions app/pages/project/instances/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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: () => {
Expand Down Expand Up @@ -132,17 +134,17 @@ export function InstancePage() {
{ enabled: !!primaryVpcId }
)

const actions = useMemo(
const allMenuActions = useMemo(
() => [
{
label: 'Copy ID',
onActivate() {
window.navigator.clipboard.writeText(instance.id || '')
},
},
...makeActions(instance),
...makeMenuActions(instance),
],
[instance, makeActions]
[instance, makeMenuActions]
)

const memory = filesize(instance.memory, { output: 'object', base: 2 })
Expand All @@ -152,9 +154,23 @@ export function InstancePage() {
<PageHeader>
<PageTitle icon={<Instances24Icon />}>{instance.name}</PageTitle>
<div className="inline-flex gap-2">
<InstanceDocsPopover />
<RefreshButton onClick={refreshData} />
<MoreActionsMenu label="Instance actions" actions={actions} />
<InstanceDocsPopover />
<div className="flex space-x-2 border-l pl-2 border-default">
{makeButtonActions(instance).map((action) => (
<Button
key={action.label}
variant="ghost"
size="sm"
onClick={action.onActivate}
disabled={!!action.disabled}
disabledReason={action.disabled}
>
{action.label}
</Button>
))}
</div>
<MoreActionsMenu label="Instance actions" actions={allMenuActions} />
</div>
</PageHeader>
<PropertiesTable.Group className="-mt-8 mb-16">
Expand Down
2 changes: 1 addition & 1 deletion app/table/columns/action-col.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: Item) => Array<MenuAction>
type MakeActions<Item> = (item: Item) => Array<MenuAction>

export type MenuAction = {
label: string
Expand Down
4 changes: 2 additions & 2 deletions app/ui/lib/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -87,7 +87,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
return (
<Wrap
when={isDisabled && disabledReason}
with={<Tooltip content={disabledReason} ref={ref} />}
with={<Tooltip content={disabledReason} ref={ref} placement="bottom" />}
>
<button
className={cn(buttonStyle({ size, variant }), className, {
Expand Down
2 changes: 2 additions & 0 deletions app/ui/lib/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FloatingArrow,
FloatingPortal,
offset,
shift,
useDismiss,
useFloating,
useFocus,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const Tooltip = forwardRef(
*/
placement ? flip() : autoPlacement(),
offset(12),
shift({ padding: 16 }),
arrow({ element: arrowRef, padding: 12 }),
],
})
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions test/e2e/instance.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ test('can start a failed instance', async ({ page }) => {
// now start the failed one
await expectInstanceState(page, 'you-fail', 'failed')
await clickRowAction(page, 'you-fail', 'Start')
await page.getByRole('button', { name: 'Confirm' }).click()
await expectInstanceState(page, 'you-fail', 'starting')
})

Expand Down Expand Up @@ -93,6 +94,7 @@ test('can stop a starting instance, then start it again', async ({ page }) => {
await expectInstanceState(page, 'not-there-yet', 'stopped')

await clickRowAction(page, 'not-there-yet', 'Start')
await page.getByRole('button', { name: 'Confirm' }).click()
await expectInstanceState(page, 'not-there-yet', 'starting')
await expectInstanceState(page, 'not-there-yet', 'running')
})
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading