Parameter search & inline display for Past Runs and Run History pages (#3985)
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user