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
153 changes: 153 additions & 0 deletions web/mobile/src/features/variants/DeleteSelectionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {useState} from "react"

import {archiveWorkflowRevision, archiveWorkflowVariant} from "@agenta/entities/workflow"
import {message} from "@agenta/ui/app-message"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
} from "@agenta/ui/ui"
import {Trash} from "@phosphor-icons/react"
import {useQueryClient} from "@tanstack/react-query"

import {useRevisionParam} from "../revisions/useRevisionParam"

import type {RegistryDeletion} from "./registryRows"
import {useAppRevisions} from "./useAppRevisions"

const label = ({variants, revisions}: RegistryDeletion) =>
[
...variants.map((variant) => `${variant.name} (all revisions)`),
...revisions.map((revision) => `${revision.variantName} v${revision.version}`),
].join(", ")

/**
* Delete whole variants and single revisions, as the old app's Registry does. An app keeps at
* least one revision: the app is the thing to delete then. Mount it only while something is
* chosen.
*/
export const DeleteSelectionDialog = ({
deletion,
projectId,
appId,
onClose,
onDeleted,
}: {
deletion: RegistryDeletion
projectId: string
appId: string
onClose: () => void
onDeleted: () => void
}) => {
const queryClient = useQueryClient()
const [shownRevisionId, setShownRevisionId] = useRevisionParam()
// The unfiltered list: a search on the page must not make the app look smaller than it is.
const all = useAppRevisions({projectId, appId, search: ""})
const [pending, setPending] = useState(false)
const variantIds = new Set(deletion.variants.map((variant) => variant.id))
const revisionIds = new Set(deletion.revisions.map((revision) => revision.revisionId))
const everything =
!all.isPending &&
!all.hasMore &&
all.rows.every((row) => variantIds.has(row.variantId) || revisionIds.has(row.revisionId))
const checking = all.isPending

const submit = async () => {
setPending(true)
try {
for (const variant of deletion.variants) {
await archiveWorkflowVariant(projectId, variant.id)
}
for (const revision of deletion.revisions) {
await archiveWorkflowRevision(projectId, revision.revisionId)
}
} catch (error) {
console.error("Delete from the registry failed", error)
message.error("Couldn't delete everything you selected")
setPending(false)
void queryClient.invalidateQueries({queryKey: ["workflows"]})
return
}
message.success(
deletion.variants.length
? "Deleted the selected variants"
: "Deleted the selected revisions",
)
onDeleted()
onClose()
const gone = (id: string | null) =>
id !== null &&
(revisionIds.has(id) ||
all.rows.some((row) => row.revisionId === id && variantIds.has(row.variantId)))
if (gone(shownRevisionId)) setShownRevisionId(null)
await queryClient.invalidateQueries({queryKey: ["workflows"]})
}

return (
<AlertDialog
open
onOpenChange={(next) => {
if (!next && !pending) onClose()
}}
>
<AlertDialogContent
onEscapeKeyDown={(event) => {
if (pending) event.preventDefault()
}}
>
<AlertDialogHeader>
<AlertDialogTitle>Confirm deletion</AlertDialogTitle>
<AlertDialogDescription>
{everything
? "Cannot delete every revision of an app. Delete the app instead."
: `You are about to delete ${label(deletion)}.`}
</AlertDialogDescription>
</AlertDialogHeader>
{all.isError ? (
<p className="m-0 text-sm text-destructive">
Couldn't check this app's revisions.{" "}
<button
type="button"
className="underline"
onClick={() => void all.refetch()}
>
Try again
</button>
</p>
) : everything ? null : (
<p className="m-0 text-sm text-muted-foreground">
This action cannot be undone.
</p>
)}
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button variant="outline" onClick={onClose} disabled={pending}>
{everything ? "Close" : "Cancel"}
</Button>
</AlertDialogCancel>
{everything ? null : (
<AlertDialogAction asChild>
<Button
variant="destructive"
disabled={pending || checking || all.isError}
onClick={(event) => {
event.preventDefault()
void submit()
}}
>
<Trash aria-hidden size={14} />
{pending ? "Deleting…" : "Delete"}
</Button>
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
124 changes: 124 additions & 0 deletions web/mobile/src/features/variants/RegistryRowCells.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {UserAuthorLabel} from "@agenta/entities/shared"
import {VariantDetailsWithStatus} from "@agenta/entity-ui/variant"
import {FormattedDate} from "@agenta/ui/components/presentational"
import {MinusCircle, PlusCircle} from "@phosphor-icons/react"

import type {RegistryRow} from "./registryRows"
import type {AppRevisionRow} from "./useAppRevisions"
import {VariantRowMenu} from "./VariantRowMenu"

const stop = {
onClick: (event: {stopPropagation: () => void}) => event.stopPropagation(),
onKeyDown: (event: {stopPropagation: () => void}) => event.stopPropagation(),
}

/** One Registry row's cells, in column order, with the old app's columns. */
export const RegistryRowCells = ({
row,
narrow,
isLatest,
expanded,
onToggle,
canDeploy,
canDelete,
playgroundHref,
onOpen,
onDeploy,
onDelete,
}: {
row: RegistryRow
narrow: boolean
isLatest: boolean
expanded: boolean
onToggle: () => void
canDeploy: boolean
canDelete: boolean
playgroundHref: ((row: AppRevisionRow) => string) | null
onOpen: (row: AppRevisionRow) => void
onDeploy: (row: AppRevisionRow) => void
onDelete: (row: RegistryRow) => void
}) => {
const {revision} = row
const Toggle = expanded ? MinusCircle : PlusCircle
return (
<>
<span className="flex min-w-0 items-center gap-1.5 text-[14px] font-medium text-foreground">
{row.kind === "variant" ? (
row.olderCount > 0 ? (
<button
type="button"
onKeyDown={stop.onKeyDown}
onClick={(event) => {
event.stopPropagation()
onToggle()
}}
aria-expanded={expanded}
aria-label={`${expanded ? "Hide" : "Show"} older revisions of ${revision.variantName}`}
className="flex shrink-0 cursor-pointer items-center border-0 bg-transparent p-0 text-muted-foreground hover:text-foreground"
>
<Toggle aria-hidden size={14} />
</button>
) : (
<span aria-hidden className="w-3.5 shrink-0" />
)
) : row.nested ? (
<span aria-hidden className="w-7 shrink-0" />
) : null}
<span className="min-w-0 truncate">
<VariantDetailsWithStatus
variant={{id: revision.revisionId}}
variantName={revision.variantName}
revision={revision.version}
isLatest={isLatest && !narrow}
showBadges
showRevisionAsTag
hideDiscard
showStable
/>
</span>
</span>
{narrow ? null : (
<span className="block break-all text-[13px] text-foreground">
{revision.model || "—"}
</span>
)}
<span className="block truncate text-[13px] text-foreground">
<FormattedDate
date={revision.createdAt}
format={narrow ? "MMM D, YYYY" : "MMM D, YYYY, h:mm A"}
fallback="—"
/>
</span>
{narrow ? null : (
<span className="block truncate text-[13px] text-muted-foreground">
<UserAuthorLabel
userId={revision.createdById}
showYouLabel
showAvatar
fallback="—"
/>
</span>
)}
{narrow ? null : (
<span
className="block truncate text-[13px] text-muted-foreground"
title={revision.message ?? undefined}
>
{revision.message || ""}
</span>
)}
<span className="flex justify-end" {...stop}>
<VariantRowMenu
row={revision}
canDeploy={canDeploy}
canDelete={canDelete}
deleteLabel={row.kind === "variant" ? "Delete variant" : "Delete"}
playgroundHref={playgroundHref}
onOpen={onOpen}
onDeploy={onDeploy}
onDelete={() => onDelete(row)}
/>
</span>
</>
)
}
90 changes: 90 additions & 0 deletions web/mobile/src/features/variants/RegistryTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {ReactNode} from "react"

import {useMediaQuery} from "@agenta/ui/hooks"
import {ListTable, type ListTableColumn, type ListTableSelection} from "@agenta/ui/list-table"

import {RegistryRowCells} from "./RegistryRowCells"
import type {RegistryRow} from "./registryRows"
import type {AppRevisionRow} from "./useAppRevisions"

const NAME: ListTableColumn = {key: "name", label: "Name", width: "minmax(200px,2fr)"}
const ACTIONS: ListTableColumn = {key: "actions", label: "Actions", srOnly: true, width: "24px"}
const WIDE_COLUMNS: ListTableColumn[] = [
NAME,
{key: "model", label: "Model", width: "minmax(140px,1.4fr)"},
{key: "createdOn", label: "Created on", width: "minmax(150px,1fr)"},
{key: "createdBy", label: "Created by", width: "minmax(130px,1fr)"},
{key: "notes", label: "Commit notes", width: "minmax(100px,1fr)"},
ACTIONS,
]
const NARROW_COLUMNS: ListTableColumn[] = [
NAME,
{key: "createdOn", label: "Created on", width: "104px"},
ACTIONS,
]

/** Tailwind's `sm`. Below it only Name and Created on stay. */
const WIDE_QUERY = "(min-width: 640px)"

/** The beta developer mode Registry: the old app's columns, row selection, expandable variants. */
export const RegistryTable = ({
rows,
isLoading,
latestRevisionId,
expanded,
onToggle,
selection,
canDeploy,
canDelete,
playgroundHref,
onOpen,
onDeploy,
onDelete,
empty,
}: {
rows: RegistryRow[]
isLoading: boolean
latestRevisionId: string | null
/** Expanded variant ids. */
expanded: ReadonlySet<string>
onToggle: (variantId: string) => void
selection: ListTableSelection<RegistryRow>
canDeploy: boolean
canDelete: boolean
playgroundHref: ((row: AppRevisionRow) => string) | null
onOpen: (row: AppRevisionRow) => void
onDeploy: (row: AppRevisionRow) => void
onDelete: (row: RegistryRow) => void
empty: ReactNode
}) => {
const narrow = !useMediaQuery(WIDE_QUERY)
const variantIdOf = (row: RegistryRow) => row.revision.variantId || row.revision.variantName
return (
<ListTable
columns={narrow ? NARROW_COLUMNS : WIDE_COLUMNS}
minWidth={narrow ? 300 : 860}
stickyHeader
loading={isLoading}
groups={rows.length ? [{key: "all", label: null, rows}] : []}
rowKey={(row) => row.key}
onOpenRow={(row) => onOpen(row.revision)}
selection={selection}
empty={empty}
renderRow={(row) => (
<RegistryRowCells
row={row}
narrow={narrow}
isLatest={row.revision.revisionId === latestRevisionId}
expanded={expanded.has(variantIdOf(row))}
onToggle={() => onToggle(variantIdOf(row))}
canDeploy={canDeploy}
canDelete={canDelete}
playgroundHref={playgroundHref}
onOpen={onOpen}
onDeploy={onDeploy}
onDelete={onDelete}
/>
)}
/>
)
}
5 changes: 4 additions & 1 deletion web/mobile/src/features/variants/VariantRowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const VariantRowMenu = ({
row,
canDeploy,
canDelete,
deleteLabel = "Delete",
playgroundHref,
onOpen,
onDeploy,
Expand All @@ -29,6 +30,8 @@ export const VariantRowMenu = ({
row: AppRevisionRow
canDeploy: boolean
canDelete: boolean
/** The Delete item's text; the Registry says "Delete variant" on a variant row. */
deleteLabel?: string
playgroundHref: ((row: AppRevisionRow) => string) | null
onOpen: (row: AppRevisionRow) => void
onDeploy: (row: AppRevisionRow) => void
Expand Down Expand Up @@ -74,7 +77,7 @@ export const VariantRowMenu = ({
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={() => onDelete(row)}>
<Trash aria-hidden size={14} />
Delete
{deleteLabel}
</DropdownMenuItem>
</>
) : null}
Expand Down
Loading
Loading