feat(workflows, runs, api): parameter metadata search/filter/display across workflows and runs (#3718)
Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
@@ -24,7 +24,12 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import { Pencil2Icon, PlayIcon } from "@radix-ui/react-icons";
|
import {
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
MixerHorizontalIcon,
|
||||||
|
Pencil2Icon,
|
||||||
|
PlayIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
@@ -35,6 +40,15 @@ import {
|
|||||||
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
||||||
import { useWorkflowRunsQuery } from "./hooks/useWorkflowRunsQuery";
|
import { useWorkflowRunsQuery } from "./hooks/useWorkflowRunsQuery";
|
||||||
import { WorkflowActions } from "./WorkflowActions";
|
import { WorkflowActions } from "./WorkflowActions";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { RunParametersDialog } from "./workflowRun/RunParametersDialog";
|
||||||
|
|
||||||
function WorkflowPage() {
|
function WorkflowPage() {
|
||||||
const { workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
@@ -44,11 +58,15 @@ function WorkflowPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearch] = useDebounce(search, 500);
|
||||||
|
const [openRunParams, setOpenRunParams] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: workflowRuns, isLoading } = useWorkflowRunsQuery({
|
const { data: workflowRuns, isLoading } = useWorkflowRunsQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
statusFilters,
|
statusFilters,
|
||||||
page,
|
page,
|
||||||
|
search: debouncedSearch,
|
||||||
refetchOnMount: "always",
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,13 +124,31 @@ function WorkflowPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<header className="flex justify-between">
|
<header>
|
||||||
<h1 className="text-2xl">Past Runs</h1>
|
<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>
|
||||||
<StatusFilterDropdown
|
<StatusFilterDropdown
|
||||||
values={statusFilters}
|
values={statusFilters}
|
||||||
onChange={setStatusFilters}
|
onChange={setStatusFilters}
|
||||||
/>
|
/>
|
||||||
</header>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -120,6 +156,7 @@ function WorkflowPage() {
|
|||||||
<TableHead className="w-1/3">ID</TableHead>
|
<TableHead className="w-1/3">ID</TableHead>
|
||||||
<TableHead className="w-1/3">Status</TableHead>
|
<TableHead className="w-1/3">Status</TableHead>
|
||||||
<TableHead className="w-1/3">Created At</TableHead>
|
<TableHead className="w-1/3">Created At</TableHead>
|
||||||
|
<TableHead className="w-0"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -173,12 +210,43 @@ function WorkflowPage() {
|
|||||||
>
|
>
|
||||||
{basicLocalTimeFormat(workflowRun.created_at)}
|
{basicLocalTimeFormat(workflowRun.created_at)}
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<RunParametersDialog
|
||||||
|
open={openRunParams !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setOpenRunParams(null);
|
||||||
|
}}
|
||||||
|
workflowPermanentId={workflowPermanentId}
|
||||||
|
workflowRunId={openRunParams}
|
||||||
|
/>
|
||||||
<Pagination className="pt-2">
|
<Pagination className="pt-2">
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { cn } from "@/util/utils";
|
|||||||
import {
|
import {
|
||||||
LightningBoltIcon,
|
LightningBoltIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
|
MixerHorizontalIcon,
|
||||||
Pencil2Icon,
|
Pencil2Icon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -39,6 +40,7 @@ import { useState } from "react";
|
|||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { NarrativeCard } from "./components/header/NarrativeCard";
|
import { NarrativeCard } from "./components/header/NarrativeCard";
|
||||||
|
import { WorkflowParametersDialog } from "./components/WorkflowParametersDialog";
|
||||||
import { useCreateWorkflowMutation } from "./hooks/useCreateWorkflowMutation";
|
import { useCreateWorkflowMutation } from "./hooks/useCreateWorkflowMutation";
|
||||||
import { ImportWorkflowButton } from "./ImportWorkflowButton";
|
import { ImportWorkflowButton } from "./ImportWorkflowButton";
|
||||||
import { WorkflowApiResponse } from "./types/workflowTypes";
|
import { WorkflowApiResponse } from "./types/workflowTypes";
|
||||||
@@ -63,6 +65,7 @@ function Workflows() {
|
|||||||
const createWorkflowMutation = useCreateWorkflowMutation();
|
const createWorkflowMutation = useCreateWorkflowMutation();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [openWorkflowId, setOpenWorkflowId] = useState<string | null>(null);
|
||||||
const [debouncedSearch] = useDebounce(search, 500);
|
const [debouncedSearch] = useDebounce(search, 500);
|
||||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
||||||
const itemsPerPage = searchParams.get("page_size")
|
const itemsPerPage = searchParams.get("page_size")
|
||||||
@@ -79,7 +82,9 @@ function Workflows() {
|
|||||||
params.append("page", String(page));
|
params.append("page", String(page));
|
||||||
params.append("page_size", String(itemsPerPage));
|
params.append("page_size", String(itemsPerPage));
|
||||||
params.append("only_workflows", "true");
|
params.append("only_workflows", "true");
|
||||||
params.append("title", debouncedSearch);
|
if (debouncedSearch) {
|
||||||
|
params.append("search_key", debouncedSearch);
|
||||||
|
}
|
||||||
return client
|
return client
|
||||||
.get(`/workflows`, {
|
.get(`/workflows`, {
|
||||||
params,
|
params,
|
||||||
@@ -96,7 +101,9 @@ function Workflows() {
|
|||||||
params.append("page", String(page + 1));
|
params.append("page", String(page + 1));
|
||||||
params.append("page_size", String(itemsPerPage));
|
params.append("page_size", String(itemsPerPage));
|
||||||
params.append("only_workflows", "true");
|
params.append("only_workflows", "true");
|
||||||
params.append("title", debouncedSearch);
|
if (debouncedSearch) {
|
||||||
|
params.append("search_key", debouncedSearch);
|
||||||
|
}
|
||||||
return client
|
return client
|
||||||
.get(`/workflows`, {
|
.get(`/workflows`, {
|
||||||
params,
|
params,
|
||||||
@@ -198,7 +205,7 @@ function Workflows() {
|
|||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
setParamPatch({ page: "1" });
|
setParamPatch({ page: "1" });
|
||||||
}}
|
}}
|
||||||
placeholder="Search by title..."
|
placeholder="Search by title or parameter..."
|
||||||
className="w-48 pl-9 lg:w-72"
|
className="w-48 pl-9 lg:w-72"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,6 +319,29 @@ function Workflows() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<WorkflowActions workflow={workflow} />
|
<WorkflowActions workflow={workflow} />
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setOpenWorkflowId(
|
||||||
|
workflow.workflow_permanent_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!workflow.workflow_definition.parameters.some(
|
||||||
|
(p) => p.parameter_type !== "output",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MixerHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>View Parameters</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -320,6 +350,14 @@ function Workflows() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<WorkflowParametersDialog
|
||||||
|
open={openWorkflowId !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setOpenWorkflowId(null);
|
||||||
|
}}
|
||||||
|
workflowId={openWorkflowId}
|
||||||
|
workflows={workflows}
|
||||||
|
/>
|
||||||
<div className="relative px-3 py-3">
|
<div className="relative px-3 py-3">
|
||||||
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
||||||
<span className="text-slate-400">Items per page</span>
|
<span className="text-slate-400">Items per page</span>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type ParameterItem = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
description?: string | null;
|
||||||
|
type?: string | null;
|
||||||
|
value?: string | null; // safe display value only; never raw secrets
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title?: string;
|
||||||
|
sectionLabel?: string;
|
||||||
|
items: Array<ParameterItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ParametersDialogBase({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title = "Parameters",
|
||||||
|
sectionLabel = "Parameters",
|
||||||
|
items,
|
||||||
|
}: Props) {
|
||||||
|
const [revealedIds, setRevealedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
function toggleReveal(id: string) {
|
||||||
|
setRevealedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRow(item: ParameterItem) {
|
||||||
|
const revealed =
|
||||||
|
item.value !== undefined &&
|
||||||
|
item.value !== null &&
|
||||||
|
item.value !== "" &&
|
||||||
|
revealedIds.has(item.id);
|
||||||
|
const isRevealable =
|
||||||
|
item.value !== undefined && item.value !== null && item.value !== "";
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="rounded-md border p-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="break-all font-mono text-sm">{item.key}</div>
|
||||||
|
{item.description ? (
|
||||||
|
<div className="text-xs text-slate-400">
|
||||||
|
— {item.description}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{item.type ? <Badge variant="secondary">{item.type}</Badge> : null}
|
||||||
|
{isRevealable ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => toggleReveal(item.id)}
|
||||||
|
title={revealed ? "Hide value" : "Show value"}
|
||||||
|
>
|
||||||
|
{revealed ? (
|
||||||
|
<EyeClosedIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<EyeOpenIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isRevealable ? (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded bg-slate-elevation2 p-2 font-mono text-xs">
|
||||||
|
{revealed ? item.value : "••••••"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="text-sm text-slate-400">No parameters.</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-xs">{sectionLabel}</Label>
|
||||||
|
<ScrollArea>
|
||||||
|
<ScrollAreaViewport className="max-h-[420px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((it) => renderRow(it))}
|
||||||
|
</div>
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { ParametersDialogBase } from "./ParametersDialogBase";
|
||||||
|
import {
|
||||||
|
WorkflowApiResponse,
|
||||||
|
WorkflowParameter,
|
||||||
|
WorkflowParameterTypes,
|
||||||
|
Parameter,
|
||||||
|
CredentialParameter,
|
||||||
|
AWSSecretParameter,
|
||||||
|
OnePasswordCredentialParameter,
|
||||||
|
AzureVaultCredentialParameter,
|
||||||
|
BitwardenLoginCredentialParameter,
|
||||||
|
BitwardenSensitiveInformationParameter,
|
||||||
|
BitwardenCreditCardDataParameter,
|
||||||
|
ContextParameter,
|
||||||
|
} from "../types/workflowTypes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
workflowId: string | null;
|
||||||
|
workflows: Array<WorkflowApiResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getParameterId(param: Parameter): string {
|
||||||
|
if ("workflow_parameter_id" in param && param.workflow_parameter_id)
|
||||||
|
return param.workflow_parameter_id;
|
||||||
|
if ("credential_parameter_id" in param && param.credential_parameter_id)
|
||||||
|
return param.credential_parameter_id;
|
||||||
|
if ("aws_secret_parameter_id" in param && param.aws_secret_parameter_id)
|
||||||
|
return param.aws_secret_parameter_id;
|
||||||
|
if (
|
||||||
|
"onepassword_credential_parameter_id" in param &&
|
||||||
|
param.onepassword_credential_parameter_id
|
||||||
|
)
|
||||||
|
return param.onepassword_credential_parameter_id;
|
||||||
|
if (
|
||||||
|
"azure_vault_credential_parameter_id" in param &&
|
||||||
|
param.azure_vault_credential_parameter_id
|
||||||
|
)
|
||||||
|
return param.azure_vault_credential_parameter_id;
|
||||||
|
if (
|
||||||
|
"bitwarden_login_credential_parameter_id" in param &&
|
||||||
|
param.bitwarden_login_credential_parameter_id
|
||||||
|
)
|
||||||
|
return param.bitwarden_login_credential_parameter_id;
|
||||||
|
if (
|
||||||
|
"bitwarden_sensitive_information_parameter_id" in param &&
|
||||||
|
param.bitwarden_sensitive_information_parameter_id
|
||||||
|
)
|
||||||
|
return param.bitwarden_sensitive_information_parameter_id;
|
||||||
|
if (
|
||||||
|
"bitwarden_credit_card_data_parameter_id" in param &&
|
||||||
|
param.bitwarden_credit_card_data_parameter_id
|
||||||
|
)
|
||||||
|
return param.bitwarden_credit_card_data_parameter_id;
|
||||||
|
if ("output_parameter_id" in param && param.output_parameter_id)
|
||||||
|
return param.output_parameter_id;
|
||||||
|
return param.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParameterDisplayType(param: Parameter): string {
|
||||||
|
return param.parameter_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParameterDisplayValue(param: Parameter): string | null {
|
||||||
|
switch (param.parameter_type) {
|
||||||
|
case "workflow": {
|
||||||
|
const p = param as WorkflowParameter;
|
||||||
|
const value = p.default_value;
|
||||||
|
try {
|
||||||
|
return value === null || value === undefined
|
||||||
|
? ""
|
||||||
|
: typeof value === "string"
|
||||||
|
? value
|
||||||
|
: JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "credential": {
|
||||||
|
// Show referenced credential id; do not reveal secrets
|
||||||
|
return "credential_id" in param
|
||||||
|
? String((param as CredentialParameter).credential_id)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
case "aws_secret": {
|
||||||
|
// Show the AWS secret key reference only
|
||||||
|
return "aws_key" in param
|
||||||
|
? String((param as AWSSecretParameter).aws_key)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
case "onepassword": {
|
||||||
|
const p = param as OnePasswordCredentialParameter;
|
||||||
|
if (p.vault_id && p.item_id) return `${p.vault_id} / ${p.item_id}`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case "azure_vault_credential": {
|
||||||
|
const p = param as AzureVaultCredentialParameter;
|
||||||
|
return p.vault_name ? `${p.vault_name}` : null;
|
||||||
|
}
|
||||||
|
case "bitwarden_login_credential": {
|
||||||
|
const p = param as BitwardenLoginCredentialParameter;
|
||||||
|
return p.bitwarden_item_id ?? p.bitwarden_collection_id ?? null;
|
||||||
|
}
|
||||||
|
case "bitwarden_sensitive_information": {
|
||||||
|
const p = param as BitwardenSensitiveInformationParameter;
|
||||||
|
return p.bitwarden_identity_key ?? null;
|
||||||
|
}
|
||||||
|
case "bitwarden_credit_card_data": {
|
||||||
|
const p = param as BitwardenCreditCardDataParameter;
|
||||||
|
return p.bitwarden_item_id ?? null;
|
||||||
|
}
|
||||||
|
case "context": {
|
||||||
|
const p = param as ContextParameter;
|
||||||
|
if ("value" in p && p.value !== undefined) {
|
||||||
|
try {
|
||||||
|
return typeof p.value === "string"
|
||||||
|
? p.value
|
||||||
|
: JSON.stringify(p.value);
|
||||||
|
} catch {
|
||||||
|
return String(p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row rendering moved inside component to access local reveal state
|
||||||
|
|
||||||
|
export function WorkflowParametersDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
workflowId,
|
||||||
|
workflows,
|
||||||
|
}: Props) {
|
||||||
|
const workflow = useMemo(
|
||||||
|
() => workflows?.find((w) => w.workflow_permanent_id === workflowId),
|
||||||
|
[workflows, workflowId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const params = workflow
|
||||||
|
? (workflow.workflow_definition.parameters.filter(
|
||||||
|
(p) =>
|
||||||
|
p.parameter_type === WorkflowParameterTypes.Workflow ||
|
||||||
|
p.parameter_type === "credential" ||
|
||||||
|
p.parameter_type === "aws_secret" ||
|
||||||
|
p.parameter_type === "onepassword" ||
|
||||||
|
p.parameter_type === "azure_vault_credential" ||
|
||||||
|
p.parameter_type === "bitwarden_login_credential" ||
|
||||||
|
p.parameter_type === "bitwarden_sensitive_information" ||
|
||||||
|
p.parameter_type === "bitwarden_credit_card_data" ||
|
||||||
|
p.parameter_type === "context",
|
||||||
|
) as Parameter[])
|
||||||
|
: ([] as Parameter[]);
|
||||||
|
return params.map((param) => ({
|
||||||
|
id: getParameterId(param),
|
||||||
|
key: param.key,
|
||||||
|
description:
|
||||||
|
"description" in param ? param.description ?? undefined : undefined,
|
||||||
|
type: getParameterDisplayType(param),
|
||||||
|
value: getParameterDisplayValue(param),
|
||||||
|
}));
|
||||||
|
}, [workflow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParametersDialogBase
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Parameters"
|
||||||
|
sectionLabel="Workflow-level parameters"
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,19 +14,27 @@ type Props = {
|
|||||||
workflowPermanentId?: string;
|
workflowPermanentId?: string;
|
||||||
statusFilters?: Array<Status>;
|
statusFilters?: Array<Status>;
|
||||||
page: number;
|
page: number;
|
||||||
|
search?: string;
|
||||||
} & UseQueryOptions;
|
} & UseQueryOptions;
|
||||||
|
|
||||||
function useWorkflowRunsQuery({
|
function useWorkflowRunsQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
statusFilters,
|
statusFilters,
|
||||||
page,
|
page,
|
||||||
|
search,
|
||||||
...queryOptions
|
...queryOptions
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
|
|
||||||
return useQuery<Array<WorkflowRunApiResponse>>({
|
return useQuery<Array<WorkflowRunApiResponse>>({
|
||||||
queryKey: ["workflowRuns", { statusFilters }, workflowPermanentId, page],
|
queryKey: [
|
||||||
|
"workflowRuns",
|
||||||
|
{ statusFilters },
|
||||||
|
workflowPermanentId,
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const client = await getClient(credentialGetter);
|
const client = await getClient(credentialGetter);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -42,6 +50,9 @@ function useWorkflowRunsQuery({
|
|||||||
params.append("status", status);
|
params.append("status", status);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (search) {
|
||||||
|
params.append("search_key", search);
|
||||||
|
}
|
||||||
|
|
||||||
return client
|
return client
|
||||||
.get(`/workflows/${workflowPermanentId}/runs`, {
|
.get(`/workflows/${workflowPermanentId}/runs`, {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { ParametersDialogBase } from "../components/ParametersDialogBase";
|
||||||
|
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||||
|
import { WorkflowRunStatusApiResponse } from "@/api/types";
|
||||||
|
import { Parameter } from "../types/workflowTypes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
workflowPermanentId?: string;
|
||||||
|
workflowRunId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RunParametersDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
workflowPermanentId,
|
||||||
|
workflowRunId,
|
||||||
|
}: Props) {
|
||||||
|
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const { data: workflow } = useWorkflowQuery({ workflowPermanentId });
|
||||||
|
const { data: run } = useQuery<WorkflowRunStatusApiResponse>({
|
||||||
|
queryKey: ["workflowRun", workflowPermanentId, workflowRunId, "dialog"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const isGlobalWorkflow = globalWorkflows?.some(
|
||||||
|
(workflow) => workflow.workflow_permanent_id === workflowPermanentId,
|
||||||
|
);
|
||||||
|
if (isGlobalWorkflow) {
|
||||||
|
params.set("template", "true");
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
.get(`/workflows/${workflowPermanentId}/runs/${workflowRunId}`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
enabled: !!workflowPermanentId && !!workflowRunId && !!globalWorkflows,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defByKey = new Map(
|
||||||
|
(workflow?.workflow_definition.parameters ?? []).map((p: Parameter) => [
|
||||||
|
p.key,
|
||||||
|
p,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = Object.entries(run?.parameters ?? {}).map(([key, value]) => {
|
||||||
|
const def = defByKey.get(key);
|
||||||
|
const description =
|
||||||
|
def && "description" in def ? def.description ?? undefined : undefined;
|
||||||
|
const type = def ? def.parameter_type ?? undefined : undefined;
|
||||||
|
const displayValue =
|
||||||
|
value === null || value === undefined
|
||||||
|
? ""
|
||||||
|
: typeof value === "string"
|
||||||
|
? value
|
||||||
|
: JSON.stringify(value);
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
key,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
value: displayValue,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParametersDialogBase
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Run Parameters"
|
||||||
|
sectionLabel="Input parameters for this run"
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Any, List, Literal, Sequence, overload
|
from typing import Any, List, Literal, Sequence, overload
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, asc, case, delete, distinct, func, or_, pool, select, tuple_, update
|
from sqlalchemy import and_, asc, case, delete, distinct, exists, func, or_, pool, select, tuple_, update
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
@@ -1574,11 +1574,17 @@ class AgentDB:
|
|||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
only_saved_tasks: bool = False,
|
only_saved_tasks: bool = False,
|
||||||
only_workflows: bool = False,
|
only_workflows: bool = False,
|
||||||
title: str = "",
|
search_key: str | None = None,
|
||||||
statuses: list[WorkflowStatus] | None = None,
|
statuses: list[WorkflowStatus] | None = None,
|
||||||
) -> list[Workflow]:
|
) -> list[Workflow]:
|
||||||
"""
|
"""
|
||||||
Get all workflows with the latest version for the organization.
|
Get all workflows with the latest version for the organization.
|
||||||
|
|
||||||
|
Search semantics:
|
||||||
|
- If `search_key` is provided, its value is used as a unified search term for both
|
||||||
|
`workflows.title` and workflow parameter metadata (key, description, and default_value).
|
||||||
|
- If `search_key` is not provided, no search filtering is applied.
|
||||||
|
- Parameter metadata search excludes soft-deleted parameter rows across parameter tables.
|
||||||
"""
|
"""
|
||||||
if page < 1:
|
if page < 1:
|
||||||
raise ValueError(f"Page must be greater than 0, got {page}")
|
raise ValueError(f"Page must be greater than 0, got {page}")
|
||||||
@@ -1609,10 +1615,133 @@ class AgentDB:
|
|||||||
main_query = main_query.where(WorkflowModel.is_saved_task.is_(True))
|
main_query = main_query.where(WorkflowModel.is_saved_task.is_(True))
|
||||||
elif only_workflows:
|
elif only_workflows:
|
||||||
main_query = main_query.where(WorkflowModel.is_saved_task.is_(False))
|
main_query = main_query.where(WorkflowModel.is_saved_task.is_(False))
|
||||||
if title:
|
|
||||||
main_query = main_query.where(WorkflowModel.title.ilike(f"%{title}%"))
|
|
||||||
if statuses:
|
if statuses:
|
||||||
main_query = main_query.where(WorkflowModel.status.in_(statuses))
|
main_query = main_query.where(WorkflowModel.status.in_(statuses))
|
||||||
|
if search_key:
|
||||||
|
search_like = f"%{search_key}%"
|
||||||
|
title_like = WorkflowModel.title.ilike(search_like)
|
||||||
|
|
||||||
|
parameter_filters = [
|
||||||
|
# WorkflowParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(WorkflowParameterModel)
|
||||||
|
.where(WorkflowParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(WorkflowParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
WorkflowParameterModel.key.ilike(search_like),
|
||||||
|
WorkflowParameterModel.description.ilike(search_like),
|
||||||
|
WorkflowParameterModel.default_value.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# OutputParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(OutputParameterModel)
|
||||||
|
.where(OutputParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(OutputParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
OutputParameterModel.key.ilike(search_like),
|
||||||
|
OutputParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# AWSSecretParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(AWSSecretParameterModel)
|
||||||
|
.where(AWSSecretParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(AWSSecretParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
AWSSecretParameterModel.key.ilike(search_like),
|
||||||
|
AWSSecretParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# BitwardenLoginCredentialParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(BitwardenLoginCredentialParameterModel)
|
||||||
|
.where(BitwardenLoginCredentialParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(BitwardenLoginCredentialParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
BitwardenLoginCredentialParameterModel.key.ilike(search_like),
|
||||||
|
BitwardenLoginCredentialParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# BitwardenSensitiveInformationParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(BitwardenSensitiveInformationParameterModel)
|
||||||
|
.where(BitwardenSensitiveInformationParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(BitwardenSensitiveInformationParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
BitwardenSensitiveInformationParameterModel.key.ilike(search_like),
|
||||||
|
BitwardenSensitiveInformationParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# BitwardenCreditCardDataParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(BitwardenCreditCardDataParameterModel)
|
||||||
|
.where(BitwardenCreditCardDataParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(BitwardenCreditCardDataParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
BitwardenCreditCardDataParameterModel.key.ilike(search_like),
|
||||||
|
BitwardenCreditCardDataParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# OnePasswordCredentialParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(OnePasswordCredentialParameterModel)
|
||||||
|
.where(OnePasswordCredentialParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(OnePasswordCredentialParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
OnePasswordCredentialParameterModel.key.ilike(search_like),
|
||||||
|
OnePasswordCredentialParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# AzureVaultCredentialParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(AzureVaultCredentialParameterModel)
|
||||||
|
.where(AzureVaultCredentialParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(AzureVaultCredentialParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
AzureVaultCredentialParameterModel.key.ilike(search_like),
|
||||||
|
AzureVaultCredentialParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# CredentialParameterModel
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(CredentialParameterModel)
|
||||||
|
.where(CredentialParameterModel.workflow_id == WorkflowModel.workflow_id)
|
||||||
|
.where(CredentialParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
CredentialParameterModel.key.ilike(search_like),
|
||||||
|
CredentialParameterModel.description.ilike(search_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
main_query = main_query.where(or_(title_like, or_(*parameter_filters)))
|
||||||
main_query = (
|
main_query = (
|
||||||
main_query.order_by(WorkflowModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
main_query.order_by(WorkflowModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
||||||
)
|
)
|
||||||
@@ -1987,7 +2116,11 @@ class AgentDB:
|
|||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
status: list[WorkflowRunStatus] | None = None,
|
status: list[WorkflowRunStatus] | None = None,
|
||||||
|
search_key: str | None = None,
|
||||||
) -> list[WorkflowRun]:
|
) -> list[WorkflowRun]:
|
||||||
|
"""
|
||||||
|
Get runs for a workflow, with optional `search_key` on parameter key/description/value.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
async with self.Session() as session:
|
async with self.Session() as session:
|
||||||
db_page = page - 1 # offset logic is 0 based
|
db_page = page - 1 # offset logic is 0 based
|
||||||
@@ -1997,6 +2130,29 @@ class AgentDB:
|
|||||||
.filter(WorkflowRunModel.workflow_permanent_id == workflow_permanent_id)
|
.filter(WorkflowRunModel.workflow_permanent_id == workflow_permanent_id)
|
||||||
.filter(WorkflowRunModel.organization_id == organization_id)
|
.filter(WorkflowRunModel.organization_id == organization_id)
|
||||||
)
|
)
|
||||||
|
if search_key:
|
||||||
|
key_like = f"%{search_key}%"
|
||||||
|
# Filter runs where any run parameter matches by key/description/value
|
||||||
|
# Use EXISTS to avoid duplicate rows and to keep pagination correct
|
||||||
|
param_exists = exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(WorkflowRunParameterModel)
|
||||||
|
.join(
|
||||||
|
WorkflowParameterModel,
|
||||||
|
WorkflowParameterModel.workflow_parameter_id
|
||||||
|
== WorkflowRunParameterModel.workflow_parameter_id,
|
||||||
|
)
|
||||||
|
.where(WorkflowRunParameterModel.workflow_run_id == WorkflowRunModel.workflow_run_id)
|
||||||
|
.where(WorkflowParameterModel.deleted_at.is_(None))
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
WorkflowParameterModel.key.ilike(key_like),
|
||||||
|
WorkflowParameterModel.description.ilike(key_like),
|
||||||
|
WorkflowRunParameterModel.value.ilike(key_like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query = query.where(param_exists)
|
||||||
if status:
|
if status:
|
||||||
query = query.filter(WorkflowRunModel.status.in_(status))
|
query = query.filter(WorkflowRunModel.status.in_(status))
|
||||||
query = query.order_by(WorkflowRunModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
query = query.order_by(WorkflowRunModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
||||||
|
|||||||
@@ -1679,8 +1679,15 @@ async def get_workflow_runs_by_id(
|
|||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(10, ge=1),
|
page_size: int = Query(10, ge=1),
|
||||||
status: Annotated[list[WorkflowRunStatus] | None, Query()] = None,
|
status: Annotated[list[WorkflowRunStatus] | None, Query()] = None,
|
||||||
|
search_key: str | None = Query(
|
||||||
|
None,
|
||||||
|
description="Search runs by parameter key, parameter description, or run parameter value.",
|
||||||
|
),
|
||||||
current_org: Organization = Depends(org_auth_service.get_current_org),
|
current_org: Organization = Depends(org_auth_service.get_current_org),
|
||||||
) -> list[WorkflowRun]:
|
) -> list[WorkflowRun]:
|
||||||
|
"""
|
||||||
|
Get workflow runs for a specific workflow permanent id.
|
||||||
|
"""
|
||||||
analytics.capture("skyvern-oss-agent-workflow-runs-get")
|
analytics.capture("skyvern-oss-agent-workflow-runs-get")
|
||||||
return await app.WORKFLOW_SERVICE.get_workflow_runs_for_workflow_permanent_id(
|
return await app.WORKFLOW_SERVICE.get_workflow_runs_for_workflow_permanent_id(
|
||||||
workflow_permanent_id=workflow_id,
|
workflow_permanent_id=workflow_id,
|
||||||
@@ -1688,6 +1695,7 @@ async def get_workflow_runs_by_id(
|
|||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
status=status,
|
status=status,
|
||||||
|
search_key=search_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1800,15 +1808,29 @@ async def get_workflows(
|
|||||||
page_size: int = Query(10, ge=1),
|
page_size: int = Query(10, ge=1),
|
||||||
only_saved_tasks: bool = Query(False),
|
only_saved_tasks: bool = Query(False),
|
||||||
only_workflows: bool = Query(False),
|
only_workflows: bool = Query(False),
|
||||||
title: str = Query(""),
|
search_key: str | None = Query(
|
||||||
|
None,
|
||||||
|
description="Unified search across workflow title and parameter metadata (key, description, default_value).",
|
||||||
|
),
|
||||||
|
title: str = Query("", deprecated=True, description="Deprecated: use search_key instead."),
|
||||||
current_org: Organization = Depends(org_auth_service.get_current_org),
|
current_org: Organization = Depends(org_auth_service.get_current_org),
|
||||||
template: bool = Query(False),
|
template: bool = Query(False),
|
||||||
) -> list[Workflow]:
|
) -> list[Workflow]:
|
||||||
"""
|
"""
|
||||||
Get all workflows with the latest version for the organization.
|
Get all workflows with the latest version for the organization.
|
||||||
|
|
||||||
|
Search semantics:
|
||||||
|
- If `search_key` is provided, its value is used as a unified search term for both
|
||||||
|
`workflows.title` and workflow parameter metadata (key, description, and default_value for
|
||||||
|
`WorkflowParameterModel`).
|
||||||
|
- Falls back to deprecated `title` (title-only search) if `search_key` is not provided.
|
||||||
|
- Parameter metadata search excludes soft-deleted parameter rows across all parameter tables.
|
||||||
"""
|
"""
|
||||||
analytics.capture("skyvern-oss-agent-workflows-get")
|
analytics.capture("skyvern-oss-agent-workflows-get")
|
||||||
|
|
||||||
|
# Determine the effective search term: prioritize search_key, fallback to title
|
||||||
|
effective_search = search_key or (title if title else None)
|
||||||
|
|
||||||
if template:
|
if template:
|
||||||
global_workflows_permanent_ids = await app.STORAGE.retrieve_global_workflows()
|
global_workflows_permanent_ids = await app.STORAGE.retrieve_global_workflows()
|
||||||
if not global_workflows_permanent_ids:
|
if not global_workflows_permanent_ids:
|
||||||
@@ -1817,7 +1839,7 @@ async def get_workflows(
|
|||||||
workflow_permanent_ids=global_workflows_permanent_ids,
|
workflow_permanent_ids=global_workflows_permanent_ids,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
title=title,
|
search_key=effective_search or "",
|
||||||
statuses=[WorkflowStatus.published, WorkflowStatus.draft],
|
statuses=[WorkflowStatus.published, WorkflowStatus.draft],
|
||||||
)
|
)
|
||||||
return workflows
|
return workflows
|
||||||
@@ -1834,7 +1856,7 @@ async def get_workflows(
|
|||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
only_saved_tasks=only_saved_tasks,
|
only_saved_tasks=only_saved_tasks,
|
||||||
only_workflows=only_workflows,
|
only_workflows=only_workflows,
|
||||||
title=title,
|
search_key=effective_search,
|
||||||
statuses=[WorkflowStatus.published, WorkflowStatus.draft],
|
statuses=[WorkflowStatus.published, WorkflowStatus.draft],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1137,7 +1137,7 @@ class WorkflowService:
|
|||||||
organization_id: str | None = None,
|
organization_id: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
title: str = "",
|
search_key: str = "",
|
||||||
statuses: list[WorkflowStatus] | None = None,
|
statuses: list[WorkflowStatus] | None = None,
|
||||||
) -> list[Workflow]:
|
) -> list[Workflow]:
|
||||||
return await app.DATABASE.get_workflows_by_permanent_ids(
|
return await app.DATABASE.get_workflows_by_permanent_ids(
|
||||||
@@ -1145,7 +1145,7 @@ class WorkflowService:
|
|||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
title=title,
|
title=search_key,
|
||||||
statuses=statuses,
|
statuses=statuses,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1156,11 +1156,14 @@ class WorkflowService:
|
|||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
only_saved_tasks: bool = False,
|
only_saved_tasks: bool = False,
|
||||||
only_workflows: bool = False,
|
only_workflows: bool = False,
|
||||||
title: str = "",
|
search_key: str | None = None,
|
||||||
statuses: list[WorkflowStatus] | None = None,
|
statuses: list[WorkflowStatus] | None = None,
|
||||||
) -> list[Workflow]:
|
) -> list[Workflow]:
|
||||||
"""
|
"""
|
||||||
Get all workflows with the latest version for the organization.
|
Get all workflows with the latest version for the organization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_key: Unified search term for title and parameter metadata (replaces title/parameter).
|
||||||
"""
|
"""
|
||||||
return await app.DATABASE.get_workflows_by_organization_id(
|
return await app.DATABASE.get_workflows_by_organization_id(
|
||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
@@ -1168,7 +1171,7 @@ class WorkflowService:
|
|||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
only_saved_tasks=only_saved_tasks,
|
only_saved_tasks=only_saved_tasks,
|
||||||
only_workflows=only_workflows,
|
only_workflows=only_workflows,
|
||||||
title=title,
|
search_key=search_key,
|
||||||
statuses=statuses,
|
statuses=statuses,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1327,6 +1330,7 @@ class WorkflowService:
|
|||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
status: list[WorkflowRunStatus] | None = None,
|
status: list[WorkflowRunStatus] | None = None,
|
||||||
|
search_key: str | None = None,
|
||||||
) -> list[WorkflowRun]:
|
) -> list[WorkflowRun]:
|
||||||
return await app.DATABASE.get_workflow_runs_for_workflow_permanent_id(
|
return await app.DATABASE.get_workflow_runs_for_workflow_permanent_id(
|
||||||
workflow_permanent_id=workflow_permanent_id,
|
workflow_permanent_id=workflow_permanent_id,
|
||||||
@@ -1334,6 +1338,7 @@ class WorkflowService:
|
|||||||
page=page,
|
page=page,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
status=status,
|
status=status,
|
||||||
|
search_key=search_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_workflow_run(
|
async def create_workflow_run(
|
||||||
|
|||||||
Reference in New Issue
Block a user