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:
parent
be18f6d03a
commit
7ffec1a696
8 changed files with 174 additions and 94 deletions
|
|
@ -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,114 +81,142 @@ 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);
|
||||||
setIsCopied(true);
|
if (currentTab) {
|
||||||
|
navigator.clipboard.writeText(currentTab.code).then(() => {
|
||||||
|
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" />
|
||||||
|
<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" />
|
{isCopied ? (
|
||||||
{tab.title}
|
<IconComponent
|
||||||
</TabsTrigger>
|
name="Check"
|
||||||
))}
|
className="h-5 w-5 text-muted-foreground"
|
||||||
</TabsList>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<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>
|
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -43,31 +43,68 @@ 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
|
||||||
.split("\n")
|
const detectedPlatform =
|
||||||
.map((line, index) => (index === 0 ? line : " " + line))
|
platform ||
|
||||||
.join("\n\t\t");
|
(/Windows|Win32|Win64|WOW32|WOW64/i.test(navigator.userAgent)
|
||||||
|
? "powershell"
|
||||||
|
: "unix");
|
||||||
|
|
||||||
return `${
|
const singleLinePayload = JSON.stringify(processedPayload);
|
||||||
isAuthenticated
|
|
||||||
? `# Get API key from environment variable
|
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
|
if [ -z "$LANGFLOW_API_KEY" ]; then
|
||||||
echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables."
|
echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}curl --request POST \\
|
}curl --request POST \\
|
||||||
--url '${apiUrl}?stream=false' \\
|
--url '${apiUrl}?stream=false' \\
|
||||||
--header 'Content-Type: application/json' \\${
|
--header 'Content-Type: application/json' \\${
|
||||||
isAuthenticated
|
isAuthenticated
|
||||||
|
|
@ -75,5 +112,6 @@ fi
|
||||||
--header "x-api-key: $LANGFLOW_API_KEY" \\`
|
--header "x-api-key: $LANGFLOW_API_KEY" \\`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
--data '${formattedJsonPayload}'`;
|
--data '${unixFormattedPayload}'`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue