Support downloading files via HTTP Calls (for Centria) (#4440)

This commit is contained in:
Marc Kelechava
2026-01-13 12:12:38 -08:00
committed by GitHub
parent a6f0781491
commit e6a3858096
16 changed files with 240 additions and 48 deletions

View File

@@ -442,7 +442,10 @@ function WorkflowRun() {
<ScrollArea>
<ScrollAreaViewport className="max-h-[250px] space-y-2">
{fileUrls.length > 0 ? (
fileUrls.map((url, index) => {
fileUrls.map((url) => {
// Extract filename from URL path, stripping query params from signed URLs
const urlPath = url.split("?")[0] ?? url;
const filename = urlPath.split("/").pop() || "download";
return (
<div key={url} title={url} className="flex gap-2">
<FileIcon className="size-6" />
@@ -450,7 +453,7 @@ function WorkflowRun() {
href={url}
className="underline underline-offset-4"
>
<span>{`File ${index + 1}`}</span>
<span>{filename}</span>
</a>
</div>
);

View File

@@ -152,12 +152,15 @@ function DebuggerRunOutput() {
<h1 className="text-sm font-bold">Workflow Run Downloaded Files</h1>
<div className="space-y-2">
{fileUrls.length > 0 ? (
fileUrls.map((url, index) => {
fileUrls.map((url) => {
// Extract filename from URL path, stripping query params from signed URLs
const urlPath = url.split("?")[0] ?? url;
const filename = urlPath.split("/").pop() || "download";
return (
<div key={url} title={url} className="flex gap-2">
<FileIcon className="size-6" />
<a href={url} className="underline underline-offset-4">
<span>{`File ${index + 1}`}</span>
<span>{filename}</span>
</a>
</div>
);

View File

@@ -67,6 +67,8 @@ const filesTooltip =
const timeoutTooltip = "Request timeout in seconds.";
const followRedirectsTooltip =
"Whether to automatically follow HTTP redirects.";
const downloadFilenameTooltip =
"The complete filename (without extension) for downloaded files. Extension is automatically determined from the response Content-Type.";
function HttpRequestNode({ id, data, type }: NodeProps<HttpRequestNodeType>) {
const { editable } = data;
@@ -431,6 +433,43 @@ function HttpRequestNode({ id, data, type }: NodeProps<HttpRequestNodeType>) {
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Save Response as File
</Label>
<HelpTooltip content="When enabled, the response body will be saved as a file instead of being parsed as JSON/text." />
</div>
<Switch
checked={data.saveResponseAsFile}
onCheckedChange={(checked) => {
update({ saveResponseAsFile: checked });
}}
disabled={!editable}
/>
</div>
{data.saveResponseAsFile && (
<div className="space-y-2 border-l-2 border-slate-600 pl-4">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Download Filename
</Label>
<HelpTooltip content={downloadFilenameTooltip} />
</div>
<Input
type="text"
value={data.downloadFilename}
onChange={(e) => {
update({ downloadFilename: e.target.value });
}}
placeholder="Auto-generated from URL"
className="nopan text-xs"
disabled={!editable}
/>
</div>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>

View File

@@ -11,6 +11,8 @@ export type HttpRequestNodeData = NodeBaseData & {
timeout: number;
followRedirects: boolean;
parameterKeys: Array<string>;
downloadFilename: string;
saveResponseAsFile: boolean;
};
export type HttpRequestNode = Node<HttpRequestNodeData, "http_request">;
@@ -29,6 +31,8 @@ export const httpRequestNodeDefaultData: HttpRequestNodeData = {
parameterKeys: [],
editable: true,
model: null,
downloadFilename: "",
saveResponseAsFile: false,
};
export function isHttpRequestNode(node: Node): node is HttpRequestNode {

View File

@@ -834,6 +834,8 @@ function convertToNode(
timeout: block.timeout,
followRedirects: block.follow_redirects,
parameterKeys: block.parameters.map((p) => p.key),
downloadFilename: block.download_filename ?? "",
saveResponseAsFile: block.save_response_as_file ?? false,
},
};
}
@@ -2325,6 +2327,8 @@ function getWorkflowBlock(
timeout: node.data.timeout,
follow_redirects: node.data.followRedirects,
parameter_keys: node.data.parameterKeys,
download_filename: node.data.downloadFilename || null,
save_response_as_file: node.data.saveResponseAsFile,
};
}
case "conditional": {
@@ -3327,6 +3331,7 @@ function convertBlocksToBlockYAML(
timeout: block.timeout,
follow_redirects: block.follow_redirects,
parameter_keys: block.parameters.map((p) => p.key),
download_filename: block.download_filename,
};
return blockYaml;
}

View File

@@ -550,6 +550,8 @@ export type HttpRequestBlock = WorkflowBlockBase & {
timeout: number;
follow_redirects: boolean;
parameters: Array<WorkflowParameter>;
download_filename: string | null;
save_response_as_file: boolean;
};
export type WorkflowDefinition = {

View File

@@ -400,4 +400,6 @@ export type HttpRequestBlockYAML = BlockYAMLBase & {
timeout: number;
follow_redirects: boolean;
parameter_keys?: Array<string> | null;
download_filename?: string | null;
save_response_as_file?: boolean;
};

View File

@@ -152,12 +152,15 @@ function WorkflowRunOutput() {
<h1 className="text-lg font-bold">Workflow Run Downloaded Files</h1>
<div className="space-y-2">
{fileUrls.length > 0 ? (
fileUrls.map((url, index) => {
fileUrls.map((url) => {
// Extract filename from URL path, stripping query params from signed URLs
const urlPath = url.split("?")[0] ?? url;
const filename = urlPath.split("/").pop() || "download";
return (
<div key={url} title={url} className="flex gap-2">
<FileIcon className="size-6" />
<a href={url} className="underline underline-offset-4">
<span>{`File ${index + 1}`}</span>
<span>{filename}</span>
</a>
</div>
);