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

View file

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

View file

@ -43,16 +43,53 @@ export function getNewCurlCode({
isAuthenticated, isAuthenticated,
endpointName, endpointName,
processedPayload, processedPayload,
platform,
}: { }: {
flowId: string; flowId: string;
isAuthenticated: boolean; isAuthenticated: boolean;
endpointName: string; endpointName: string;
processedPayload: any; processedPayload: any;
platform?: "unix" | "powershell";
}): string { }): string {
const { protocol, host } = customGetHostProtocol(); const { protocol, host } = customGetHostProtocol();
const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`;
const formattedJsonPayload = JSON.stringify(processedPayload, null, 2) // Auto-detect if no platform specified
const detectedPlatform =
platform ||
(/Windows|Win32|Win64|WOW32|WOW64/i.test(navigator.userAgent)
? "powershell"
: "unix");
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") .split("\n")
.map((line, index) => (index === 0 ? line : " " + line)) .map((line, index) => (index === 0 ? line : " " + line))
.join("\n\t\t"); .join("\n\t\t");
@ -75,5 +112,6 @@ fi
--header "x-api-key: $LANGFLOW_API_KEY" \\` --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; @apply flex items-center justify-between px-2 py-2;
} }
.api-modal-tabs-content { .api-modal-tabs-content {
@apply -mt-1 h-full w-full pb-4; @apply h-full w-full;
} }
.api-modal-accordion-display { .api-modal-accordion-display {
@apply mt-2 flex h-full w-full; @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.getByRole("heading", { name: "Basic Prompting" }).click();
await page.getByTestId("publish-button").click(); await page.getByTestId("publish-button").click();
await page.getByTestId("api-access-item").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(); await page.getByTestId("icon-Copy").click();
const handle = await page.evaluateHandle(() => const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(), navigator.clipboard.readText(),
@ -35,7 +35,7 @@ test(
await page.getByText("Close").last().click(); 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(); await page.getByTestId("icon-Copy").click();
const handle2 = await page.evaluateHandle(() => const handle2 = await page.evaluateHandle(() =>
navigator.clipboard.readText(), navigator.clipboard.readText(),

View file

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

View file

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

View file

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