diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index ec8c6702c..cdd63525d 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -4,14 +4,15 @@ import { useState, type CSSProperties, } from "react"; -import { ArrowPathIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; +import { ArrowPathIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; import { Link, Outlet, useLocation, useMatches } from "react-router"; +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { InterviewDock } from "../components/interview-dock"; import { SteerComposer } from "../components/steer-composer"; import { ErrorState } from "../components/state"; import { useToast } from "../components/toast"; -import { PRIMARY_BUTTON_CLASS, SECONDARY_BUTTON_CLASS } from "../components/ui"; +import { SECONDARY_BUTTON_CLASS } from "../components/ui"; import { isRunStatus, mapRunSummaryToRunItem, @@ -50,12 +51,15 @@ const allTabs = [ export const handle = { hideHeader: true }; -const CANCEL_BUTTON_CLASS = - "inline-flex items-center justify-center gap-2 rounded-lg border border-coral/30 bg-coral/10 px-4 py-2 text-sm font-medium text-coral transition-colors hover:bg-coral/15 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-coral/10"; - -const MUTATION_BUTTON_CLASS = +const ACTIONS_TRIGGER_CLASS = `${SECONDARY_BUTTON_CLASS} disabled:cursor-not-allowed disabled:opacity-60`; +const MENU_ITEM_CLASS = + "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-fg-3 transition-colors data-focus:bg-overlay data-focus:text-fg data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; + +const MENU_ITEM_DANGER_CLASS = + "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-coral transition-colors data-focus:bg-coral/10 data-focus:text-coral data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; + function classNames(...classes: Array) { return classes.filter(Boolean).join(" "); } @@ -237,78 +241,25 @@ export default function RunDetail({ params }: { params: { id: string } }) { -
- {statusKind === "running" && ( -
- -
- )} - - {visibility.showPrimaryCancel && ( -
- -
- )} - - {visibility.showArchive && ( -
- -
- )} - - {visibility.showUnarchive && ( -
- -
- )} - - {run.sandboxId && ( -
- -
- )} -
+ setSteerOpen(true)} + canPreview={!!run.sandboxId} + previewPending={previewPending} + onPreview={() => void previewMutation.trigger({ + port: 3000, + expires_in_secs: 3600, + })} + canArchive={visibility.showArchive} + archivePending={archivePending} + onArchive={() => void archiveMutation.trigger()} + canUnarchive={visibility.showUnarchive} + unarchivePending={unarchivePending} + onUnarchive={() => void unarchiveMutation.trigger()} + canCancel={visibility.showPrimaryCancel} + cancelPending={cancelPending} + onCancel={() => void cancelMutation.trigger()} + />
void; + canPreview: boolean; + previewPending: boolean; + onPreview: () => void; + canArchive: boolean; + archivePending: boolean; + onArchive: () => void; + canUnarchive: boolean; + unarchivePending: boolean; + onUnarchive: () => void; + canCancel: boolean; + cancelPending: boolean; + onCancel: () => void; +} + +function ActionsMenu(props: ActionsMenuProps) { + const { + canSteer, onSteer, + canPreview, previewPending, onPreview, + canArchive, archivePending, onArchive, + canUnarchive, unarchivePending, onUnarchive, + canCancel, cancelPending, onCancel, + } = props; + + const hasOps = canPreview || canSteer; + const hasLifecycle = canArchive || canUnarchive; + const hasDestructive = canCancel; + const hasAny = hasOps || hasLifecycle || hasDestructive; + const anyPending = previewPending || archivePending || unarchivePending || cancelPending; + + if (!hasAny) return null; + + return ( + + + {anyPending && + + {canPreview && ( + + + + )} + {canSteer && ( + + + + )} + {hasOps && hasLifecycle &&
} + {canArchive && ( + + + + )} + {canUnarchive && ( + + + + )} + {(hasOps || hasLifecycle) && hasDestructive && ( +
+ )} + {canCancel && ( + + + + )} + +
+ ); +}