feat: add powershell curl tab to UI (#8889)

* chore: Bump version to 1.5.0 and update dependencies

- Updated langflow version to 1.5.0 in pyproject.toml, package.json, and package-lock.json.
- Updated langflow-base dependency to version 0.5.0.
- Added platform markers for several dependencies in uv.lock to improve compatibility across different systems.

* fix: fixes auth check for auto_login  (#8796)

* ref: improve docling template updates and error message (#8837)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>

* Attempt to provide powershell curl command

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Added OS selector to code tabs

* Added no select classes to API modal

*  (code-tabs.tsx): add data-testid attribute to API tab elements for testing purposes
🔧 (tweaksTest.spec.ts, curlApiGeneration.spec.ts, pythonApiGeneration.spec.ts, generalBugs-shard-3.spec.ts): update test scripts to use data-testid attribute for API tab elements instead of role attribute

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Jordan Frazier 2025-07-07 13:52:48 -07:00 committed by Gabriel Luiz Freitas Almeida
commit 7ffec1a696
8 changed files with 174 additions and 94 deletions

View file

@ -1,11 +1,12 @@
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button";
import useAuthStore from "@/stores/authStore";
import useFlowStore from "@/stores/flowStore";
import { useTweaksStore } from "@/stores/tweaksStore";
import { tabsArrayType } from "@/types/tabs";
import { hasStreaming } from "@/utils/reactflowUtils";
import { getOS } from "@/utils/utils";
import { useEffect, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import {
@ -19,6 +20,19 @@ import { getNewCurlCode } from "../utils/get-curl-code";
import { getNewJsApiCode } from "../utils/get-js-api-code";
import { getNewPythonApiCode } from "../utils/get-python-api-code";
const operatingSystemTabs = [
{
name: "macoslinux",
title: "macOS/Linux",
icon: "FaApple",
},
{
name: "windows",
title: "Windows",
icon: "FaWindows",
},
];
export default function APITabsComponent() {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const endpointName = useFlowStore(
@ -67,114 +81,142 @@ export default function APITabsComponent() {
isAuthenticated: autologin || false,
processedPayload: processedPayload,
};
const tabsList: tabsArrayType = [
// Platform selection for cURL
const [selectedPlatform, setSelectedPlatform] = useState(
operatingSystemTabs.find((tab) =>
tab.name.includes(getOS() === "windows" ? "windows" : "macoslinux"),
)?.name || "macoslinux",
);
const tabsList = [
{
title: "Python",
icon: "BWPython",
language: "python",
code: getNewPythonApiCode(codeOptions),
copyCode: getNewPythonApiCode(codeOptions),
},
{
title: "JavaScript",
icon: "javascript",
language: "javascript",
code: getNewJsApiCode(codeOptions),
copyCode: getNewJsApiCode(codeOptions),
},
{
title: "cURL",
icon: "TerminalSquare",
language: "shell",
code: getNewCurlCode(codeOptions),
copyCode: getNewCurlCode(codeOptions),
language: selectedPlatform === "windows" ? "powershell" : "shell",
code: getNewCurlCode({
...codeOptions,
platform: selectedPlatform === "windows" ? "powershell" : "unix",
}),
},
];
const [activeTab, setActiveTab] = useState<number>(0);
const [selectedTab, setSelectedTab] = useState("Python");
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(tabsList[activeTab].code).then(() => {
setIsCopied(true);
const currentTab = tabsList.find((tab) => tab.title === selectedTab);
if (currentTab) {
navigator.clipboard.writeText(currentTab.code).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
}
};
useEffect(() => {
setIsCopied(false);
}, [activeTab]);
}, [selectedTab, selectedPlatform]);
const currentTab = tabsList.find((tab) => tab.title === selectedTab);
return (
<Tabs
value={activeTab.toString()}
className={"api-modal-tabs inset-0 m-0"}
onValueChange={(value) => {
setActiveTab(parseInt(value));
}}
>
<div className="flex items-center justify-between">
{tabsList.length > 0 && tabsList[0].title !== "" ? (
<TabsList className="flex w-fit items-center rounded bg-muted p-1">
{tabsList.map((tab, index) => (
<TabsTrigger
key={index}
value={index.toString()}
className="flex items-center gap-2.5 rounded-md !border-0 px-4 py-2 !text-sm data-[state=active]:bg-background"
<div className="api-modal-tabs inset-0 m-0 h-full overflow-hidden">
<div className="flex h-full flex-col gap-4 overflow-hidden">
{/* Main language tabs */}
<div className="flex flex-row justify-start border-b border-border">
{tabsList.map((tab) => (
<Button
unstyled
key={tab.title}
className={`flex h-8 select-none flex-row items-center gap-2 text-nowrap border-b-2 border-border border-b-transparent !py-1 font-medium ${
selectedTab === tab.title
? "border-b-2 border-black dark:border-b-white"
: "text-muted-foreground hover:text-foreground"
} px-3 py-2 text-[13px]`}
onClick={() => setSelectedTab(tab.title)}
data-testid={`api_tab_${tab.title.toLowerCase()}`}
>
<IconComponent name={tab.icon} className="h-4 w-4" />
<span>{tab.title}</span>
</Button>
))}
</div>
{/* Platform selection for cURL */}
{selectedTab === "cURL" && (
<div className="flex flex-col gap-4">
<Tabs value={selectedPlatform} onValueChange={setSelectedPlatform}>
<TabsList>
{operatingSystemTabs.map((tab, index) => (
<TabsTrigger
className="flex select-none items-center gap-2"
key={index}
value={tab.name}
>
<IconComponent name={tab.icon} aria-hidden="true" />
{tab.title}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
)}
{/* Code content */}
{currentTab && (
<div className="api-modal-tabs-content overflow-hidden">
<div className="relative flex h-full w-full">
<Button
variant="ghost"
size="icon"
onClick={copyToClipboard}
data-testid="btn-copy-code"
className="!hover:bg-foreground group absolute right-4 top-2 z-10 select-none"
>
<IconComponent name={tab.icon} className="h-4 w-4" />
{tab.title}
</TabsTrigger>
))}
</TabsList>
) : (
<div></div>
{isCopied ? (
<IconComponent
name="Check"
className="h-5 w-5 text-muted-foreground"
/>
) : (
<IconComponent
name="Copy"
className="!h-5 !w-5 text-muted-foreground"
/>
)}
</Button>
<SyntaxHighlighter
showLineNumbers={true}
wrapLongLines={true}
language={currentTab.language}
style={dark ? oneDark : oneLight}
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll"
>
{currentTab.code}
</SyntaxHighlighter>
</div>
</div>
)}
</div>
{tabsList.map((tab, idx) => (
<TabsContent
value={idx.toString()}
className="api-modal-tabs-content mt-4 overflow-hidden"
key={idx}
>
<div className="relative flex h-full w-full">
<Button
variant="ghost"
size="icon"
onClick={copyToClipboard}
data-testid="btn-copy-code"
className="!hover:bg-foreground group absolute right-4 top-2"
>
{isCopied ? (
<IconComponent
name="Check"
className="h-5 w-5 text-muted-foreground"
/>
) : (
<IconComponent
name="Copy"
className="!h-5 !w-5 text-muted-foreground"
/>
)}
</Button>
<SyntaxHighlighter
showLineNumbers={true}
wrapLongLines={true}
language={tab.language}
style={dark ? oneDark : oneLight}
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll"
>
{tab.code}
</SyntaxHighlighter>
</div>
</TabsContent>
))}
</Tabs>
</div>
);
}

View file

@ -135,7 +135,7 @@ export default function ApiModal({
<Button
variant="ghost"
size="icon"
className="h-8 px-3"
className="h-8 select-none px-3"
onClick={() => setOpenTweaks(true)}
data-testid="tweaks-button"
>

View file

@ -43,31 +43,68 @@ export function getNewCurlCode({
isAuthenticated,
endpointName,
processedPayload,
platform,
}: {
flowId: string;
isAuthenticated: boolean;
endpointName: string;
processedPayload: any;
platform?: "unix" | "powershell";
}): string {
const { protocol, host } = customGetHostProtocol();
const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`;
const formattedJsonPayload = JSON.stringify(processedPayload, null, 2)
.split("\n")
.map((line, index) => (index === 0 ? line : " " + line))
.join("\n\t\t");
// Auto-detect if no platform specified
const detectedPlatform =
platform ||
(/Windows|Win32|Win64|WOW32|WOW64/i.test(navigator.userAgent)
? "powershell"
: "unix");
return `${
isAuthenticated
? `# Get API key from environment variable
const singleLinePayload = JSON.stringify(processedPayload);
if (detectedPlatform === "powershell") {
// PowerShell with here-string (most robust for complex JSON)
return `${
isAuthenticated
? `if (-not $env:LANGFLOW_API_KEY) {
Write-Error "LANGFLOW_API_KEY environment variable not found"
exit 1
}
`
: ""
}$jsonData = @'
${singleLinePayload}
'@
curl --request POST \`
--url "${apiUrl}?stream=false" \`
--header "Content-Type: application/json"${
isAuthenticated
? ` \`
--header "x-api-key: $env:LANGFLOW_API_KEY"`
: ""
} \`
--data $jsonData`;
} else {
// Unix-like systems (Linux, Mac, WSL2)
const unixFormattedPayload = JSON.stringify(processedPayload, null, 2)
.split("\n")
.map((line, index) => (index === 0 ? line : " " + line))
.join("\n\t\t");
return `${
isAuthenticated
? `# Get API key from environment variable
if [ -z "$LANGFLOW_API_KEY" ]; then
echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables."
exit 1
fi
`
: ""
}curl --request POST \\
: ""
}curl --request POST \\
--url '${apiUrl}?stream=false' \\
--header 'Content-Type: application/json' \\${
isAuthenticated
@ -75,5 +112,6 @@ fi
--header "x-api-key: $LANGFLOW_API_KEY" \\`
: ""
}
--data '${formattedJsonPayload}'`;
--data '${unixFormattedPayload}'`;
}
}

View file

@ -905,7 +905,7 @@
@apply flex items-center justify-between px-2 py-2;
}
.api-modal-tabs-content {
@apply -mt-1 h-full w-full pb-4;
@apply h-full w-full;
}
.api-modal-accordion-display {
@apply mt-2 flex h-full w-full;

View file

@ -12,7 +12,7 @@ test(
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.getByTestId("publish-button").click();
await page.getByTestId("api-access-item").click();
await page.getByRole("tab", { name: "cURL" }).click();
await page.getByTestId("api_tab_curl").click();
await page.getByTestId("icon-Copy").click();
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),
@ -35,7 +35,7 @@ test(
await page.getByText("Close").last().click();
await page.getByRole("tab", { name: "cURL" }).click();
await page.getByTestId("api_tab_curl").click();
await page.getByTestId("icon-Copy").click();
const handle2 = await page.evaluateHandle(() =>
navigator.clipboard.readText(),

View file

@ -11,7 +11,7 @@ test(
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.getByTestId("publish-button").click();
await page.getByTestId("api-access-item").click();
await page.getByRole("tab", { name: "cURL" }).click();
await page.getByTestId("api_tab_curl").click();
await page.getByTestId("icon-Copy").last().click();
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),

View file

@ -11,7 +11,7 @@ test(
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.getByTestId("publish-button").click();
await page.getByTestId("api-access-item").click();
await page.getByRole("tab", { name: "Python" }).click();
await page.getByTestId("api_tab_python").click();
await page.getByTestId("icon-Copy").click();
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),

View file

@ -99,7 +99,7 @@ test(
await page.getByTestId("button-send").click();
await page.getByRole("tab", { name: "python" }).isVisible({
await page.getByTestId("api_tab_python").isVisible({
timeout: 100000,
});