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 = {
label: string;
to: string;
icon?: React.ReactNode;
};
type Props = {
@@ -23,13 +24,18 @@ function SwitchBarNavigation({ options }: Props) {
key={option.to}
className={({ isActive }) => {
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,
},
);
}}
>
{option.icon && (
<span className="mr-1 flex items-center justify-center">
{option.icon}
</span>
)}
{option.label}
</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 { ProxyLocation, Status } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation";
import {
SwitchBarNavigation,
type SwitchBarNavigationOption,
} from "@/components/SwitchBarNavigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -19,6 +23,7 @@ import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { apiBaseUrl } from "@/util/env";
import {
CodeIcon,
FileIcon,
Pencil2Icon,
PlayIcon,
@@ -38,6 +43,9 @@ import { cn } from "@/util/utils";
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
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() {
const [searchParams, setSearchParams] = useSearchParams();
@@ -53,7 +61,7 @@ function WorkflowRun() {
workflowPermanentId,
});
const hasScript = false;
const cacheKey = workflow?.cache_key ?? "";
const {
data: workflowRun,
@@ -61,6 +69,44 @@ function WorkflowRun() {
isFetched,
} = 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 cancelWorkflowMutation = useMutation({
@@ -208,7 +254,9 @@ function WorkflowRun() {
webhookFailureReasonData) &&
workflowRun.status === Status.Completed;
const switchBarOptions = [
const isGeneratingCode = !isFinalized && !hasPublishedCode;
const switchBarOptions: SwitchBarNavigationOption[] = [
{
label: "Overview",
to: "overview",
@@ -225,14 +273,16 @@ function WorkflowRun() {
label: "Recording",
to: "recording",
},
];
if (!hasScript) {
switchBarOptions.push({
{
label: "Code",
to: "code",
});
}
icon: !isGeneratingCode ? (
<CodeIcon className="inline-block size-5" />
) : (
<ReloadIcon className="inline-block size-5 animate-spin" />
),
},
];
return (
<div className="space-y-8">

View File

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

View File

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