Parameter search & inline display for Past Runs and Run History pages (#3985)

This commit is contained in:
Celal Zamanoglu
2025-11-14 01:33:39 +03:00
committed by GitHub
parent f1e118ab2b
commit a95837783a
10 changed files with 797 additions and 257 deletions

View File

@@ -25,12 +25,11 @@ import {
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
import { cn } from "@/util/utils";
import {
MagnifyingGlassIcon,
MixerHorizontalIcon,
Pencil2Icon,
PlayIcon,
} from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import {
Link,
useNavigate,
@@ -40,7 +39,6 @@ import {
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
import { useWorkflowRunsQuery } from "./hooks/useWorkflowRunsQuery";
import { WorkflowActions } from "./WorkflowActions";
import { Input } from "@/components/ui/input";
import { useDebounce } from "use-debounce";
import {
Tooltip,
@@ -50,6 +48,15 @@ import {
} from "@/components/ui/tooltip";
import { RunParametersDialog } from "./workflowRun/RunParametersDialog";
import * as env from "@/util/env";
import { getClient } from "@/api/AxiosClient";
import { WorkflowRunStatusApiResponse } from "@/api/types";
import { useQuery } from "@tanstack/react-query";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useGlobalWorkflowsQuery } from "./hooks/useGlobalWorkflowsQuery";
import { TableSearchInput } from "@/components/TableSearchInput";
import { useKeywordSearch } from "./hooks/useKeywordSearch";
import { useParameterExpansion } from "./hooks/useParameterExpansion";
import { ParameterDisplayInline } from "./components/ParameterDisplayInline";
function WorkflowPage() {
const { workflowPermanentId } = useParams();
@@ -62,6 +69,13 @@ function WorkflowPage() {
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search, 500);
const [openRunParams, setOpenRunParams] = useState<string | null>(null);
const { matchesParameter, isSearchActive } =
useKeywordSearch(debouncedSearch);
const {
expandedRows,
toggleExpanded: toggleParametersExpanded,
setAutoExpandedRows,
} = useParameterExpansion();
const { data: workflowRuns, isLoading } = useWorkflowRunsQuery({
workflowPermanentId,
@@ -83,6 +97,20 @@ function WorkflowPage() {
workflowPermanentId,
});
useEffect(() => {
if (!isSearchActive) {
setAutoExpandedRows([]);
return;
}
const runIds =
workflowRuns
?.map((run) => run.workflow_run_id)
.filter((id): id is string => Boolean(id)) ?? [];
setAutoExpandedRows(runIds);
}, [isSearchActive, workflowRuns, setAutoExpandedRows]);
if (!workflowPermanentId) {
return null; // this should never happen
}
@@ -129,22 +157,17 @@ function WorkflowPage() {
<h1 className="text-2xl">Past Runs</h1>
</header>
<div className="flex items-center justify-between gap-4">
<div className="relative">
<div className="absolute left-0 top-0 flex h-9 w-9 items-center justify-center">
<MagnifyingGlassIcon className="size-5" />
</div>
<Input
value={search}
onChange={(e) => {
setSearch(e.target.value);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
setSearchParams(params, { replace: true });
}}
placeholder="Search runs by parameter..."
className="w-48 pl-9 lg:w-72"
/>
</div>
<TableSearchInput
value={search}
onChange={(value) => {
setSearch(value);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
setSearchParams(params, { replace: true });
}}
placeholder="Search runs by parameter..."
className="w-48 lg:w-72"
/>
<StatusFilterDropdown
values={statusFilters}
onChange={setStatusFilters}
@@ -154,20 +177,20 @@ function WorkflowPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-1/3">ID</TableHead>
<TableHead className="w-1/3">Status</TableHead>
<TableHead className="w-1/3">Created At</TableHead>
<TableHead className="w-0"></TableHead>
<TableHead className="w-1/4">ID</TableHead>
<TableHead className="w-1/4">Status</TableHead>
<TableHead className="w-1/4">Created At</TableHead>
<TableHead className="w-1/4"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={3}>Loading...</TableCell>
<TableCell colSpan={4}>Loading...</TableCell>
</TableRow>
) : workflowRuns?.length === 0 ? (
<TableRow>
<TableCell colSpan={3}>No workflow runs found</TableCell>
<TableCell colSpan={4}>No workflow runs found</TableCell>
</TableRow>
) : (
workflowRuns?.map((workflowRun) => {
@@ -183,59 +206,90 @@ function WorkflowPage() {
workflowRun.workflow_run_id ?? ""
);
return (
<TableRow
key={workflowRun.workflow_run_id}
onClick={(event) => {
const url = env.useNewRunsUrl
? `/runs/${workflowRun.workflow_run_id}`
: `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`;
const isExpanded = expandedRows.has(
workflowRun.workflow_run_id,
);
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin + url,
"_blank",
"noopener,noreferrer",
);
return;
}
navigate(url);
}}
className="cursor-pointer"
>
<TableCell>{workflowRunId}</TableCell>
<TableCell>
<StatusBadge status={workflowRun.status} />
</TableCell>
<TableCell
title={basicTimeFormat(workflowRun.created_at)}
return (
<React.Fragment key={workflowRun.workflow_run_id}>
{/* Main run row */}
<TableRow
onClick={(event) => {
const url = env.useNewRunsUrl
? `/runs/${workflowRun.workflow_run_id}`
: `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`;
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin + url,
"_blank",
"noopener,noreferrer",
);
return;
}
navigate(url);
}}
className="cursor-pointer"
>
{basicLocalTimeFormat(workflowRun.created_at)}
</TableCell>
<TableCell>
<div className="flex justify-end">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.stopPropagation();
setOpenRunParams(
workflowRun.workflow_run_id ?? null,
);
}}
>
<MixerHorizontalIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>View Parameters</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</TableCell>
</TableRow>
<TableCell>{workflowRunId}</TableCell>
<TableCell>
<StatusBadge status={workflowRun.status} />
</TableCell>
<TableCell
title={basicTimeFormat(workflowRun.created_at)}
>
{basicLocalTimeFormat(workflowRun.created_at)}
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={(event) => {
event.stopPropagation();
toggleParametersExpanded(
workflowRun.workflow_run_id,
);
}}
className={cn(
isExpanded && "text-blue-400",
)}
>
<MixerHorizontalIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
{isExpanded
? "Hide Parameters"
: "Show Parameters"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</TableCell>
</TableRow>
{/* Expanded parameters section */}
{isExpanded && (
<TableRow key={`${workflowRun.workflow_run_id}-params`}>
<TableCell
colSpan={4}
className="bg-slate-50 dark:bg-slate-900/50"
>
<WorkflowRunParameters
workflowPermanentId={workflowPermanentId}
workflowRunId={workflowRun.workflow_run_id}
workflow={workflow}
searchQuery={debouncedSearch}
keywordMatchesParameter={matchesParameter}
/>
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})
)}
@@ -292,4 +346,94 @@ function WorkflowPage() {
);
}
type WorkflowRunParametersProps = {
workflowPermanentId: string;
workflowRunId: string;
workflow: Awaited<ReturnType<typeof useWorkflowQuery>>["data"];
searchQuery: string;
keywordMatchesParameter: (parameter: {
key: string;
value: unknown;
description?: string | null;
}) => boolean;
};
function WorkflowRunParameters({
workflowPermanentId,
workflowRunId,
workflow,
searchQuery,
keywordMatchesParameter,
}: WorkflowRunParametersProps) {
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
const credentialGetter = useCredentialGetter();
const isGlobalWorkflow =
globalWorkflows?.some(
(wf) => wf.workflow_permanent_id === workflowPermanentId,
) ?? false;
const { data: run, isLoading } = useQuery<WorkflowRunStatusApiResponse>({
queryKey: [
"workflowRun",
workflowPermanentId,
workflowRunId,
"params",
isGlobalWorkflow,
],
queryFn: async () => {
const client = await getClient(credentialGetter);
const params = new URLSearchParams();
if (isGlobalWorkflow) {
params.set("template", "true");
}
return client
.get(`/workflows/${workflowPermanentId}/runs/${workflowRunId}`, {
params,
})
.then((r) => r.data);
},
enabled: Boolean(workflowPermanentId && workflowRunId),
});
if (isLoading) {
return (
<div className="ml-8 py-4">
<Skeleton className="h-20 w-full" />
</div>
);
}
if (!run || !run.parameters || Object.keys(run.parameters).length === 0) {
return (
<div className="ml-8 py-4 text-sm text-slate-400">
No parameters for this run
</div>
);
}
// Create a map of parameter definitions by key
const defByKey = new Map(
(workflow?.workflow_definition.parameters ?? []).map((p) => [p.key, p]),
);
const parameterItems = Object.entries(run.parameters).map(([key, value]) => {
const def = defByKey.get(key);
const description = def && "description" in def ? def.description : null;
return {
key,
value,
description,
};
});
return (
<ParameterDisplayInline
parameters={parameterItems}
searchQuery={searchQuery}
keywordMatchesParameter={keywordMatchesParameter}
/>
);
}
export { WorkflowPage };