Skip to content
Closed
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
2 changes: 2 additions & 0 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type {
VpcFirewallRuleUpdate,
} from './__generated__/Api'

export { snakeify } from './__generated__/util'

// API limits encoded in https://github.com/oxidecomputer/omicron/blob/9dd23096de93c7d6d05ea21f6323de4410060652/nexus/src/app/mod.rs#L142

// These are not actually used in app code, just the mock server. In the app we
Expand Down
63 changes: 63 additions & 0 deletions app/components/CopyCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useState, type ReactNode } from 'react'
import { Success12Icon } from '@oxide/design-system/icons/react'

import { Button } from '~/ui/lib/Button'
import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { Modal } from '~/ui/lib/Modal'
import { useTimeout } from '~/ui/lib/use-timeout'

Expand Down Expand Up @@ -114,3 +115,65 @@ export function EquivalentCliCommand({ project, instance }: EquivProps) {
</>
)
}

type CodeBlock = {
label: string
copyAriaLabel: string
/** Plain text that gets copied to the clipboard */
code: string
/** Optional rendered representation; falls back to `code` */
rendered?: ReactNode
}

type CliCommandModalProps = {
isOpen: boolean
onDismiss: () => void
title: string
description?: ReactNode
blocks: [CodeBlock, ...CodeBlock[]]
footer?: ReactNode
}

/**
* Modal that stacks one or more code blocks, each with its own copy-to-clipboard
* button.
*/
export function CliCommandModal({
isOpen,
onDismiss,
title,
description,
blocks,
footer,
}: CliCommandModalProps) {
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} title={title} width="free">
<Modal.Body>
{description && (
<Modal.Section>
<div className="text-sans-md text-secondary">{description}</div>
</Modal.Section>
)}
{blocks.map((block) => (
<Modal.Section key={block.label}>
<div className="flex items-center justify-between">
<div className="text-mono-sm text-secondary">{block.label}</div>
<CopyToClipboard ariaLabel={block.copyAriaLabel} text={block.code} />
</div>
<pre className="text-mono-md bg-default border-secondary max-h-80 w-full overflow-auto rounded-md border px-4 py-3 tracking-normal! normal-case!">
{block.rendered ?? block.code}
</pre>
</Modal.Section>
))}
</Modal.Body>
<Modal.Footer
onDismiss={onDismiss}
onAction={onDismiss}
actionText="Close"
showCancel={false}
>
{footer}
</Modal.Footer>
</Modal>
)
}
52 changes: 52 additions & 0 deletions app/components/ImageDetailSideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { type Image } from '@oxide/api'
import { Images16Icon } from '@oxide/design-system/icons/react'

import { ReadOnlySideModalForm } from '~/components/form/ReadOnlySideModalForm'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ResourceLabel } from '~/ui/lib/SideModal'
import { docLinks } from '~/util/links'

type ImageDetailSideModalProps = {
image: Image
onDismiss: () => void
}

export function ImageDetailSideModal({ image, onDismiss }: ImageDetailSideModalProps) {
// projectId is only set on project images; silo images leave it null
const visibility = image.projectId ? 'Project' : 'Silo'
return (
<ReadOnlySideModalForm
title="Image details"
onDismiss={onDismiss}
animate
subtitle={
<ResourceLabel>
<Images16Icon /> {image.name}
</ResourceLabel>
}
>
<PropertiesTable>
<PropertiesTable.IdRow id={image.id} />
<PropertiesTable.DescriptionRow description={image.description} sideModal />
<PropertiesTable.Row label="Visibility">{visibility}</PropertiesTable.Row>
<PropertiesTable.Row label="OS">{image.os}</PropertiesTable.Row>
<PropertiesTable.Row label="Version">{image.version}</PropertiesTable.Row>
<PropertiesTable.SizeRow bytes={image.size} />
<PropertiesTable.Row label="Block size">
{image.blockSize.toLocaleString()} bytes
</PropertiesTable.Row>
<PropertiesTable.DateRow label="Created" date={image.timeCreated} />
<PropertiesTable.DateRow label="Last Modified" date={image.timeModified} />
</PropertiesTable>
<SideModalFormDocs docs={[docLinks.images]} />
</ReadOnlySideModalForm>
)
}
77 changes: 77 additions & 0 deletions app/components/SnapshotDetailSideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { useQuery } from '@tanstack/react-query'

import { api, qErrorsAllowed, type Snapshot } from '@oxide/api'
import { Snapshots16Icon } from '@oxide/design-system/icons/react'
import { Badge } from '@oxide/design-system/ui'

import { ReadOnlySideModalForm } from '~/components/form/ReadOnlySideModalForm'
import { SnapshotStateBadge } from '~/components/StateBadge'
import { SkeletonCell } from '~/table/cells/EmptyCell'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ResourceLabel } from '~/ui/lib/SideModal'
import { docLinks } from '~/util/links'

const sourceDiskQ = (disk: string) =>
qErrorsAllowed(
api.diskView,
{ path: { disk } },
{
errorsExpected: {
explanation: 'the source disk may have been deleted.',
statusCode: 404,
},
}
)

const DiskNameFromId = ({ diskId }: { diskId: string }) => {
const { data } = useQuery(sourceDiskQ(diskId))
if (!data) return <SkeletonCell />
if (data.type === 'error') return <Badge color="neutral">Deleted</Badge>
return <>{data.data.name}</>
}

type SnapshotDetailSideModalProps = {
snapshot: Snapshot
onDismiss: () => void
}

export function SnapshotDetailSideModal({
snapshot,
onDismiss,
}: SnapshotDetailSideModalProps) {
return (
<ReadOnlySideModalForm
title="Snapshot details"
onDismiss={onDismiss}
animate
subtitle={
<ResourceLabel>
<Snapshots16Icon /> {snapshot.name}
</ResourceLabel>
}
>
<PropertiesTable>
<PropertiesTable.IdRow id={snapshot.id} />
<PropertiesTable.DescriptionRow description={snapshot.description} sideModal />
<PropertiesTable.Row label="State">
<SnapshotStateBadge state={snapshot.state} />
</PropertiesTable.Row>
<PropertiesTable.SizeRow bytes={snapshot.size} />
<PropertiesTable.Row label="Source disk">
<DiskNameFromId diskId={snapshot.diskId} />
</PropertiesTable.Row>
<PropertiesTable.DateRow label="Created" date={snapshot.timeCreated} />
<PropertiesTable.DateRow label="Last Modified" date={snapshot.timeModified} />
</PropertiesTable>
<SideModalFormDocs docs={[docLinks.snapshots]} />
</ReadOnlySideModalForm>
)
}
8 changes: 7 additions & 1 deletion app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,12 @@ export default function ImageCreate() {
const finalizeDisk = useApiMutation(api.diskFinalizeImport)
const createImage = useApiMutation(api.imageCreate)
const deleteDisk = useApiMutation(api.diskDelete)
const deleteSnapshot = useApiMutation(api.snapshotDelete)
const deleteSnapshot = useApiMutation(api.snapshotDelete, {
onSuccess() {
queryClient.invalidateEndpoint('snapshotList')
queryClient.invalidateEndpoint('snapshotView')
},
})

// TODO: Distinguish cleanup mutations being called after successful run vs.
// due to error. In the former case, they have their own steps to highlight as
Expand Down Expand Up @@ -277,6 +282,7 @@ export default function ImageCreate() {
const deleteSnapshotCleanup = useApiMutation(api.snapshotDelete, {
onSuccess() {
queryClient.invalidateEndpoint('snapshotList')
queryClient.invalidateEndpoint('snapshotView')
},
})

Expand Down
Loading
Loading