Skip to content
Open
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
58 changes: 58 additions & 0 deletions app/components/ImageDetailSideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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
/** Pass `true` for state-driven usage (e.g., DiskSourceCell). Omit for route usage. */
animate?: boolean
}

export function ImageDetailSideModal({
image,
onDismiss,
animate,
}: 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={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>
)
}
55 changes: 55 additions & 0 deletions app/components/SnapshotDetailSideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 Snapshot } from '@oxide/api'
import { Snapshots16Icon } from '@oxide/design-system/icons/react'

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

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>
)
}
66 changes: 0 additions & 66 deletions app/forms/image-edit.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export default function ImageCreate() {
const finalizeDisk = useApiMutation(api.diskFinalizeImport)
const createImage = useApiMutation(api.imageCreate)
const deleteDisk = useApiMutation(api.diskDelete)
// no invalidation needed: the deleted snapshot is the transient one created
// by this flow, so nothing can be displaying it
const deleteSnapshot = useApiMutation(api.snapshotDelete)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were some invalidations added here that were probably harmless but I'd rather avoid them because they give the impression of being necessary. On the other hand I'm always wondering #3083 whether we should be more aggressive about invalidations. But I don't think we tend to have issues.


// TODO: Distinguish cleanup mutations being called after successful run vs.
Expand Down
11 changes: 6 additions & 5 deletions app/pages/SiloImageEdit.tsx → app/pages/SiloImageDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*
* Copyright Oxide Computer Company
*/
import type { LoaderFunctionArgs } from 'react-router'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'

import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api'

import { EditImageSideModalForm } from '~/forms/image-edit'
import { ImageDetailSideModal } from '~/components/ImageDetailSideModal'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getSiloImageSelector, useSiloImageSelector } from '~/hooks/use-params'
import { pb } from '~/util/path-builder'
Expand All @@ -23,11 +23,12 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
return null
}

export const handle = titleCrumb('Edit Image')
export const handle = titleCrumb('Image')

export default function SiloImageEdit() {
export default function SiloImageDetail() {
const selector = useSiloImageSelector()
const navigate = useNavigate()
const { data } = usePrefetchedQuery(imageView(selector))

return <EditImageSideModalForm image={data} dismissLink={pb.siloImages()} type="Silo" />
return <ImageDetailSideModal image={data} onDismiss={() => navigate(pb.siloImages())} />
}
9 changes: 7 additions & 2 deletions app/pages/SiloImagesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const handle = { crumb: 'Images' }
const colHelper = createColumnHelper<Image>()
const staticCols = [
colHelper.accessor('name', {
cell: makeLinkCell((image) => pb.siloImageEdit({ image })),
cell: makeLinkCell((image) => pb.siloImage({ image })),
}),
colHelper.accessor('description', Columns.description),
colHelper.accessor('os', {
Expand All @@ -81,6 +81,7 @@ export default function SiloImagesPage() {
// prettier-ignore
addToast(<>Image <HL>{variables.path.image}</HL> deleted</>)
queryClient.invalidateEndpoint('imageList')
queryClient.invalidateEndpoint('imageView')
},
})

Expand Down Expand Up @@ -116,7 +117,7 @@ export default function SiloImagesPage() {
},
...(allImages?.items || []).map((i) => ({
value: i.name,
action: pb.siloImageEdit({ image: i.name }),
action: pb.siloImage({ image: i.name }),
navGroup: 'Go to silo image',
})),
],
Expand Down Expand Up @@ -160,6 +161,8 @@ const PromoteImageModal = ({ onDismiss }: { onDismiss: () => void }) => {
// prettier-ignore
addToast(<>Image <HL>{data.name}</HL> promoted</>)
queryClient.invalidateEndpoint('imageList')
// promotion flips projectId; refetch the per-id view
queryClient.invalidateEndpoint('imageView')
onDismiss()
},
onError: (err) => {
Expand Down Expand Up @@ -256,6 +259,8 @@ const DemoteImageModal = ({
})

queryClient.invalidateEndpoint('imageList')
// demotion flips projectId; refetch the per-id view
queryClient.invalidateEndpoint('imageView')
onDismiss()
},
onError: (err) => {
Expand Down
9 changes: 5 additions & 4 deletions app/pages/project/disks/DiskDetailSideModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { ReadOnlySideModalForm } from '~/components/form/ReadOnlySideModalForm'
import { DiskStateBadge, DiskTypeBadge } from '~/components/StateBadge'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getDiskSelector, useDiskSelector } from '~/hooks/use-params'
import { DiskSourceName } from '~/table/cells/DiskSourceCell'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ResourceLabel } from '~/ui/lib/SideModal'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
import { bytesToGiB } from '~/util/units'

const diskView = ({ disk, project }: PP.Disk) =>
q(api.diskView, { path: { disk }, query: { project } })
Expand Down Expand Up @@ -75,16 +75,17 @@ export function DiskDetailSideModal({
<PropertiesTable>
<PropertiesTable.IdRow id={disk.id} />
<PropertiesTable.DescriptionRow description={disk.description} sideModal />
<PropertiesTable.Row label="Size">{bytesToGiB(disk.size)} GiB</PropertiesTable.Row>
<PropertiesTable.SizeRow bytes={disk.size} />
<PropertiesTable.Row label="State">
<DiskStateBadge state={disk.state.state} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Disk type">
<DiskTypeBadge diskType={disk.diskType} />
</PropertiesTable.Row>
{/* TODO: show attached instance by name like the table does? */}
<PropertiesTable.IdRow id={disk.imageId} label="Image ID" />
<PropertiesTable.IdRow id={disk.snapshotId} label="Snapshot ID" />
<PropertiesTable.Row label="Source">
<DiskSourceName imageId={disk.imageId} snapshotId={disk.snapshotId} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Read only">
<Badge color="neutral">{disk.readOnly ? 'True' : 'False'}</Badge>
</PropertiesTable.Row>
Expand Down
11 changes: 11 additions & 0 deletions app/pages/project/disks/DisksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { DiskSourceName } from '~/table/cells/DiskSourceCell'
import { InstanceLink } from '~/table/cells/InstanceLinkCell'
import { LinkCell } from '~/table/cells/LinkCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
Expand Down Expand Up @@ -90,6 +91,8 @@ export default function DisksPage() {
const { mutateAsync: deleteDisk } = useApiMutation(api.diskDelete, {
onSuccess(_data, variables) {
queryClient.invalidateEndpoint('diskList')
// deleted disk may be a snapshot's source, shown in the snapshot detail modal
queryClient.invalidateEndpoint('diskView')
// prettier-ignore
addToast(<>Disk <HL>{variables.path.disk}</HL> deleted</>)
},
Expand Down Expand Up @@ -176,6 +179,14 @@ export default function DisksPage() {
cell: (info) => <DiskTypeBadge diskType={info.getValue()} />,
}),
colHelper.accessor('size', Columns.size),
colHelper.accessor(
(row) => ({ imageId: row.imageId, snapshotId: row.snapshotId }),
{
id: 'source',
header: 'Source',
cell: (info) => <DiskSourceName {...info.getValue()} />,
}
),
colHelper.accessor('state.state', {
header: 'state',
cell: (info) => <DiskStateBadge state={info.getValue()} />,
Expand Down
8 changes: 6 additions & 2 deletions app/pages/project/images/ImagesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default function ImagesPage() {
// prettier-ignore
addToast(<>Image <HL>{variables.path.image}</HL> deleted</>)
queryClient.invalidateEndpoint('imageList')
queryClient.invalidateEndpoint('imageView')
},
})

Expand Down Expand Up @@ -96,7 +97,7 @@ export default function ImagesPage() {
const columns = useMemo(() => {
return [
colHelper.accessor('name', {
cell: makeLinkCell((image) => pb.projectImageEdit({ project, image })),
cell: makeLinkCell((image) => pb.projectImage({ project, image })),
}),
colHelper.accessor('description', Columns.description),
colHelper.accessor('os', {
Expand Down Expand Up @@ -131,7 +132,7 @@ export default function ImagesPage() {
},
...(allImages?.items || []).map((i) => ({
value: i.name,
action: pb.projectImageEdit({ project, image: i.name }),
action: pb.projectImage({ project, image: i.name }),
navGroup: 'Go to project image',
})),
],
Expand Down Expand Up @@ -183,6 +184,9 @@ const PromoteImageModal = ({ onDismiss, imageName }: PromoteModalProps) => {
},
})
queryClient.invalidateEndpoint('imageList')
// promotion flips projectId; refetch the per-id view so cached entries
// reflect the new visibility
queryClient.invalidateEndpoint('imageView')
onDismiss()
},
onError: (err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*
* Copyright Oxide Computer Company
*/
import type { LoaderFunctionArgs } from 'react-router'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'

import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api'

import { EditImageSideModalForm } from '~/forms/image-edit'
import { ImageDetailSideModal } from '~/components/ImageDetailSideModal'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getProjectImageSelector, useProjectImageSelector } from '~/hooks/use-params'
import { pb } from '~/util/path-builder'
Expand All @@ -24,12 +24,13 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
return null
}

export const handle = titleCrumb('Edit Image')
export const handle = titleCrumb('Image')

export default function ProjectImageEdit() {
export default function ProjectImageDetail() {
const selector = useProjectImageSelector()
const navigate = useNavigate()
const { data } = usePrefetchedQuery(imageView(selector))

const dismissLink = pb.projectImages({ project: selector.project })
return <EditImageSideModalForm image={data} dismissLink={dismissLink} type="Project" />
return <ImageDetailSideModal image={data} onDismiss={() => navigate(dismissLink)} />
}
Loading
Loading