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:
Celal Zamanoglu
2025-10-16 16:04:53 +03:00
committed by GitHub
parent 427e674299
commit 5531367566
9 changed files with 700 additions and 18 deletions

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
}