artifacts!

This commit is contained in:
2026-03-03 13:42:41 -06:00
parent 5da940639a
commit 8299e5efcb
50 changed files with 4779 additions and 341 deletions

View File

@@ -21,6 +21,7 @@ import {
type ArtifactSummary,
type ArtifactType,
} from "@/hooks/useArtifacts";
import { useArtifactStream } from "@/hooks/useArtifactStream";
import { OpenAPI } from "@/api/core/OpenAPI";
interface ExecutionArtifactsPanelProps {
@@ -349,6 +350,11 @@ export default function ExecutionArtifactsPanel({
null,
);
// Subscribe to real-time artifact notifications for this execution.
// WebSocket-driven cache invalidation replaces most of the polling need,
// but we keep polling as a fallback (staleTime/refetchInterval in the hook).
useArtifactStream({ executionId, enabled: isRunning });
const { data, isLoading, error } = useExecutionArtifacts(
executionId,
isRunning,

View File

@@ -0,0 +1,109 @@
import { useMemo } from "react";
import { BarChart3 } from "lucide-react";
import {
useExecutionArtifacts,
type ArtifactSummary,
} from "@/hooks/useArtifacts";
import { useArtifactStream, useArtifactProgress } from "@/hooks/useArtifactStream";
interface ExecutionProgressBarProps {
executionId: number;
/** Whether the execution is still running (enables real-time updates) */
isRunning: boolean;
}
/**
* Inline progress bar for executions that have progress-type artifacts.
*
* Combines two data sources for responsiveness:
* 1. **Polling**: `useExecutionArtifacts` fetches the artifact list periodically
* so we can detect when a progress artifact first appears and read its initial state.
* 2. **WebSocket**: `useArtifactStream` subscribes to real-time `artifact_updated`
* notifications, which include the latest `progress_percent` and `progress_message`
* extracted by the database trigger — providing instant updates between polls.
*
* The WebSocket-pushed summary takes precedence when available (it's newer), with
* the polled data as a fallback for the initial render before any WS message arrives.
*
* Renders nothing if no progress artifact exists for this execution.
*/
export default function ExecutionProgressBar({
executionId,
isRunning,
}: ExecutionProgressBarProps) {
// Subscribe to real-time artifact updates for this execution
useArtifactStream({ executionId, enabled: isRunning });
// Read the latest progress pushed via WebSocket (no API call)
const wsSummary = useArtifactProgress(executionId);
// Poll-based artifact list (fallback + initial detection)
const { data } = useExecutionArtifacts(
executionId,
isRunning,
);
// Find progress artifacts from the polled data
const progressArtifact = useMemo<ArtifactSummary | null>(() => {
const artifacts = data?.data ?? [];
return artifacts.find((a) => a.type === "progress") ?? null;
}, [data]);
// If there's no progress artifact at all, render nothing
if (!progressArtifact && !wsSummary) {
return null;
}
// Prefer the WS-pushed summary (more current), fall back to indicating
// that a progress artifact exists but we haven't received detail yet.
const percent = wsSummary?.percent ?? null;
const message = wsSummary?.message ?? null;
const name = wsSummary?.name ?? progressArtifact?.name ?? "Progress";
// If we have a progress artifact but no percent yet (first poll, no WS yet),
// show an indeterminate state
const hasPercent = percent != null;
const clampedPercent = hasPercent ? Math.min(Math.max(percent, 0), 100) : 0;
const isComplete = hasPercent && clampedPercent >= 100;
return (
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="flex items-center gap-2 mb-1.5">
<BarChart3 className="h-4 w-4 text-amber-500 flex-shrink-0" />
<span className="text-sm font-medium text-gray-700 truncate">
{name}
</span>
{hasPercent && (
<span className="text-xs font-mono text-gray-500 ml-auto flex-shrink-0">
{Math.round(clampedPercent)}%
</span>
)}
</div>
{/* Progress bar */}
<div className="w-full bg-gray-200 rounded-full h-2">
{hasPercent ? (
<div
className={`h-2 rounded-full transition-all duration-500 ease-out ${
isComplete
? "bg-green-500"
: "bg-amber-500"
}`}
style={{ width: `${clampedPercent}%` }}
/>
) : (
/* Indeterminate shimmer when we know a progress artifact exists
but haven't received a percent value yet */
<div className="h-2 rounded-full bg-amber-300 animate-pulse w-full opacity-40" />
)}
</div>
{/* Message */}
{message && (
<p className="text-xs text-gray-500 mt-1 truncate" title={message}>
{message}
</p>
)}
</div>
);
}

View File

@@ -16,6 +16,9 @@ import {
SquareAsterisk,
KeyRound,
Home,
Paperclip,
FolderOpenDot,
FolderArchive,
} from "lucide-react";
// Color mappings for navigation items — defined outside component for stable reference
@@ -113,6 +116,12 @@ const navSections = [
{
items: [
{ to: "/keys", label: "Keys & Secrets", icon: KeyRound, color: "gray" },
{
to: "/artifacts",
label: "Artifacts",
icon: FolderArchive,
color: "gray",
},
{
to: "/packs",
label: "Pack Management",