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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user