workflow run UI: code generation affordances (#3521)

This commit is contained in:
Jonathan Dobson
2025-09-24 16:19:07 -04:00
committed by GitHub
parent 3c9ccbafc2
commit 0ef1419d8b
4 changed files with 100 additions and 18 deletions

View File

@@ -4,6 +4,7 @@ import { NavLink, useSearchParams } from "react-router-dom";
type Option = { type Option = {
label: string; label: string;
to: string; to: string;
icon?: React.ReactNode;
}; };
type Props = { type Props = {
@@ -23,13 +24,18 @@ function SwitchBarNavigation({ options }: Props) {
key={option.to} key={option.to}
className={({ isActive }) => { className={({ isActive }) => {
return cn( return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700", "flex cursor-pointer items-center justify-center rounded-sm px-3 py-2 text-center hover:bg-slate-700",
{ {
"bg-slate-700": isActive, "bg-slate-700": isActive,
}, },
); );
}} }}
> >
{option.icon && (
<span className="mr-1 flex items-center justify-center">
{option.icon}
</span>
)}
{option.label} {option.label}
</NavLink> </NavLink>
); );
@@ -38,4 +44,4 @@ function SwitchBarNavigation({ options }: Props) {
); );
} }
export { SwitchBarNavigation }; export { SwitchBarNavigation, type Option as SwitchBarNavigationOption };

View File

@@ -1,7 +1,11 @@
import { useEffect, useState } from "react";
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { ProxyLocation, Status } from "@/api/types"; import { ProxyLocation, Status } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge"; import { StatusBadge } from "@/components/StatusBadge";
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation"; import {
SwitchBarNavigation,
type SwitchBarNavigationOption,
} from "@/components/SwitchBarNavigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -19,6 +23,7 @@ import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { apiBaseUrl } from "@/util/env"; import { apiBaseUrl } from "@/util/env";
import { import {
CodeIcon,
FileIcon, FileIcon,
Pencil2Icon, Pencil2Icon,
PlayIcon, PlayIcon,
@@ -38,6 +43,9 @@ import { cn } from "@/util/utils";
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown"; import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
import { type ApiCommandOptions } from "@/util/apiCommands"; import { type ApiCommandOptions } from "@/util/apiCommands";
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery";
function WorkflowRun() { function WorkflowRun() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -53,7 +61,7 @@ function WorkflowRun() {
workflowPermanentId, workflowPermanentId,
}); });
const hasScript = false; const cacheKey = workflow?.cache_key ?? "";
const { const {
data: workflowRun, data: workflowRun,
@@ -61,6 +69,44 @@ function WorkflowRun() {
isFetched, isFetched,
} = useWorkflowRunQuery(); } = useWorkflowRunQuery();
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
const [hasPublishedCode, setHasPublishedCode] = useState(false);
const [cacheKeyValue, setCacheKeyValue] = useState(
cacheKey === ""
? ""
: constructCacheKeyValue({ codeKey: cacheKey, workflow, workflowRun }),
);
const { data: cacheKeyValues } = useCacheKeyValuesQuery({
cacheKey,
debounceMs: 100,
page: 1,
workflowPermanentId,
});
useEffect(() => {
setCacheKeyValue(
constructCacheKeyValue({ codeKey: cacheKey, workflow, workflowRun }) ??
cacheKeyValues?.values[0],
);
}, [cacheKey, cacheKeyValues, setCacheKeyValue, workflow, workflowRun]);
const { data: blockScriptsPublished } = useBlockScriptsQuery({
cacheKey,
cacheKeyValue,
workflowPermanentId,
pollIntervalMs: !hasPublishedCode && !isFinalized ? 3000 : undefined,
status: "published",
workflowRunId: workflowRun?.workflow_run_id,
});
useEffect(() => {
const keys = Object.keys(blockScriptsPublished ?? {});
setHasPublishedCode(keys.length > 0);
}, [blockScriptsPublished, setHasPublishedCode]);
const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery(); const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();
const cancelWorkflowMutation = useMutation({ const cancelWorkflowMutation = useMutation({
@@ -208,7 +254,9 @@ function WorkflowRun() {
webhookFailureReasonData) && webhookFailureReasonData) &&
workflowRun.status === Status.Completed; workflowRun.status === Status.Completed;
const switchBarOptions = [ const isGeneratingCode = !isFinalized && !hasPublishedCode;
const switchBarOptions: SwitchBarNavigationOption[] = [
{ {
label: "Overview", label: "Overview",
to: "overview", to: "overview",
@@ -225,14 +273,16 @@ function WorkflowRun() {
label: "Recording", label: "Recording",
to: "recording", to: "recording",
}, },
]; {
if (!hasScript) {
switchBarOptions.push({
label: "Code", label: "Code",
to: "code", to: "code",
}); icon: !isGeneratingCode ? (
} <CodeIcon className="inline-block size-5" />
) : (
<ReloadIcon className="inline-block size-5 animate-spin" />
),
},
];
return ( return (
<div className="space-y-8"> <div className="space-y-8">

View File

@@ -8,7 +8,7 @@ type Props = {
cacheKeyValue?: string; cacheKeyValue?: string;
workflowPermanentId?: string; workflowPermanentId?: string;
pollIntervalMs?: number; pollIntervalMs?: number;
status?: string; status?: "pending" | "published";
workflowRunId?: string; workflowRunId?: string;
}; };
@@ -28,7 +28,6 @@ function useBlockScriptsQuery({
workflowPermanentId, workflowPermanentId,
cacheKey, cacheKey,
cacheKeyValue, cacheKeyValue,
pollIntervalMs,
status, status,
workflowRunId, workflowRunId,
], ],

View File

@@ -19,6 +19,7 @@ import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils"; import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
import { cn } from "@/util/utils";
interface Props { interface Props {
showCacheKeyValueSelector?: boolean; showCacheKeyValueSelector?: boolean;
@@ -47,16 +48,40 @@ function WorkflowRunCode(props?: Props) {
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
const parameters = workflowRun?.parameters; const parameters = workflowRun?.parameters;
const { data: blockScripts } = useBlockScriptsQuery({ const [hasPublishedCode, setHasPublishedCode] = useState(false);
const { data: blockScriptsPending } = useBlockScriptsQuery({
cacheKey, cacheKey,
cacheKeyValue, cacheKeyValue,
workflowPermanentId, workflowPermanentId,
pollIntervalMs: !isFinalized ? 3000 : undefined, pollIntervalMs: !hasPublishedCode && !isFinalized ? 3000 : undefined,
status: "pending", status: "pending",
workflowRunId: workflowRun?.workflow_run_id, workflowRunId: workflowRun?.workflow_run_id,
}); });
const { data: blockScriptsPublished } = useBlockScriptsQuery({
cacheKey,
cacheKeyValue,
workflowPermanentId,
status: "published",
workflowRunId: workflowRun?.workflow_run_id,
});
useEffect(() => {
const keys = Object.keys(blockScriptsPublished ?? {});
setHasPublishedCode(keys.length > 0);
}, [blockScriptsPublished, setHasPublishedCode]);
const orderedBlockLabels = getOrderedBlockLabels(workflow); const orderedBlockLabels = getOrderedBlockLabels(workflow);
const code = getCode(orderedBlockLabels, blockScripts).join("").trim();
const code = getCode(
orderedBlockLabels,
hasPublishedCode ? blockScriptsPublished : blockScriptsPending,
)
.join("")
.trim();
const isGeneratingCode = !isFinalized && !hasPublishedCode;
useEffect(() => { useEffect(() => {
setCacheKeyValue( setCacheKeyValue(
@@ -93,7 +118,7 @@ function WorkflowRunCode(props?: Props) {
}); });
}, [queryClient, workflowRun, workflowPermanentId, cacheKey, cacheKeyValue]); }, [queryClient, workflowRun, workflowPermanentId, cacheKey, cacheKeyValue]);
if (code.length === 0) { if (code.length === 0 && !isGeneratingCode) {
return ( return (
<div className="flex items-center justify-center bg-slate-elevation3 p-8"> <div className="flex items-center justify-center bg-slate-elevation3 p-8">
No code has been generated yet. No code has been generated yet.
@@ -166,7 +191,9 @@ function WorkflowRunCode(props?: Props) {
</Select> </Select>
</div> </div>
<CodeEditor <CodeEditor
className="h-full w-full overflow-y-scroll" className={cn("h-full w-full overflow-y-scroll", {
"animate-pulse": isGeneratingCode,
})}
language="python" language="python"
value={code} value={code}
lineWrap={false} lineWrap={false}