Merge remote-tracking branch 'origin/dev' into chatImg
This commit is contained in:
commit
a6c038a629
471 changed files with 20280 additions and 11322 deletions
|
|
@ -1,7 +1,24 @@
|
|||
{
|
||||
"extends": ["eslint:recommended", "plugin:node/recommended"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["react", "import-helpers", "prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"project": ["./tsconfig.node.json", "./tsconfig.json"],
|
||||
"extraFileExtensions:": [".mdx"],
|
||||
"extensions:": [".mdx"]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
|
|
@ -16,6 +33,7 @@
|
|||
"no-redeclare": "error",
|
||||
"no-empty": "error",
|
||||
"no-await-in-loop": "error",
|
||||
"react/react-in-jsx-scope": 0,
|
||||
"node/exports-style": ["error", "module.exports"],
|
||||
"node/file-extension-in-import": ["error", "always"],
|
||||
"node/prefer-global/buffer": ["error", "always"],
|
||||
|
|
|
|||
4
src/frontend/.gitignore
vendored
4
src/frontend/.gitignore
vendored
|
|
@ -28,3 +28,7 @@ yarn-error.log*
|
|||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
|
|||
1
src/frontend/.prettierignore
Normal file
1
src/frontend/.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/*
|
||||
1659
src/frontend/package-lock.json
generated
1659
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@million/lint": "^0.0.73",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
|
|
@ -26,6 +27,8 @@
|
|||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@types/axios": "^0.14.0",
|
||||
"ace-builds": "^1.24.1",
|
||||
"ag-grid-community": "^31.2.1",
|
||||
"ag-grid-react": "^31.2.1",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
|
|
@ -35,20 +38,24 @@
|
|||
"dompurify": "^3.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.17.19",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.331.0",
|
||||
"million": "^3.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"openseadragon": "^4.1.1",
|
||||
"playwright": "^1.42.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.2.21",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.2.21",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-pdf": "^7.7.1",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react18-json-view": "^0.2.3",
|
||||
|
|
@ -63,6 +70,7 @@
|
|||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"zod": "^3.23.7",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -70,9 +78,12 @@
|
|||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"./**/*.{js,jsx,ts,tsx,json,md}\"",
|
||||
"format": "npx prettier --write \"{docs,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
|
||||
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx pretty-quick --staged"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
|
|
@ -93,7 +104,7 @@
|
|||
},
|
||||
"proxy": "http://127.0.0.1:7860",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.0",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.80",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
|
|
@ -116,6 +127,7 @@
|
|||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-dotted-background": "^1.1.0",
|
||||
"typescript": "^5.2.2",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ body {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
user-select: none;
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.react-flow__node {
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
|
@ -76,6 +83,25 @@ body {
|
|||
height: 8px !important;
|
||||
border-radius: 10px;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1 !important;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb !important;
|
||||
}
|
||||
|
||||
.jv-indent::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1 !important;
|
||||
|
|
@ -96,6 +122,45 @@ body {
|
|||
}
|
||||
|
||||
.custom-hover:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */
|
||||
background-color: rgba(
|
||||
99,
|
||||
102,
|
||||
241,
|
||||
0.1
|
||||
); /* Medium indigo color with 20% opacity */
|
||||
}
|
||||
|
||||
.json-view-playground .json-view {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.json-view-flow .json-view {
|
||||
background-color: #bbb !important;
|
||||
}
|
||||
|
||||
.ag-body-horizontal-scroll-viewport,
|
||||
.ag-body-vertical-scroll-viewport {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar,
|
||||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-track,
|
||||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb,
|
||||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb:hover,
|
||||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import "./App.css";
|
|||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||
import CrashErrorComponent from "./components/crashErrorComponent";
|
||||
import FetchErrorComponent from "./components/fetchErrorComponent";
|
||||
import LoadingComponent from "./components/loadingComponent";
|
||||
import {
|
||||
|
|
@ -19,16 +19,16 @@ import Router from "./routes";
|
|||
import useAlertStore from "./stores/alertStore";
|
||||
import { useDarkStore } from "./stores/darkStore";
|
||||
import useFlowsManagerStore from "./stores/flowsManagerStore";
|
||||
import { useGlobalVariablesStore } from "./stores/globalVariables";
|
||||
import { useFolderStore } from "./stores/foldersStore";
|
||||
import { useGlobalVariablesStore } from "./stores/globalVariablesStore/globalVariables";
|
||||
import { useStoreStore } from "./stores/storeStore";
|
||||
import { useTypesStore } from "./stores/typesStore";
|
||||
|
||||
export default function App() {
|
||||
const removeFromTempNotificationList = useAlertStore(
|
||||
(state) => state.removeFromTempNotificationList
|
||||
(state) => state.removeFromTempNotificationList,
|
||||
);
|
||||
const tempNotificationList = useAlertStore(
|
||||
(state) => state.tempNotificationList
|
||||
(state) => state.tempNotificationList,
|
||||
);
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||
|
|
@ -46,12 +46,15 @@ export default function App() {
|
|||
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||
const setGlobalVariables = useGlobalVariablesStore(
|
||||
(state) => state.setGlobalVariables
|
||||
(state) => state.setGlobalVariables,
|
||||
);
|
||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||
const navigate = useNavigate();
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
|
||||
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
|
||||
const loadingFolders = useFolderStore((state) => state.loading);
|
||||
|
||||
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -63,9 +66,10 @@ export default function App() {
|
|||
}, [dark]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
const isLoginPage = location.pathname.includes("login");
|
||||
|
||||
autoLogin()
|
||||
autoLogin(abortController.signal)
|
||||
.then(async (user) => {
|
||||
if (user && user["access_token"]) {
|
||||
user["refresh_token"] = "auto";
|
||||
|
|
@ -73,34 +77,54 @@ export default function App() {
|
|||
setUserData(user);
|
||||
setAutoLogin(true);
|
||||
setLoading(false);
|
||||
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
|
||||
fetchAllData();
|
||||
}
|
||||
})
|
||||
.catch(async () => {
|
||||
setAutoLogin(false);
|
||||
if (isAuthenticated && !isLoginPage) {
|
||||
getUser();
|
||||
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
|
||||
} else {
|
||||
setLoading(false);
|
||||
useFlowsManagerStore.setState({ isLoading: false });
|
||||
.catch(async (error) => {
|
||||
if (error.name !== "CanceledError") {
|
||||
setAutoLogin(false);
|
||||
if (isAuthenticated && !isLoginPage) {
|
||||
getUser();
|
||||
fetchAllData();
|
||||
} else {
|
||||
setLoading(false);
|
||||
useFlowsManagerStore.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [isAuthenticated]);
|
||||
|
||||
/*
|
||||
Abort the request as it isn't needed anymore, the component being
|
||||
unmounted. It helps avoid, among other things, the well-known "can't
|
||||
perform a React state update on an unmounted component" warning.
|
||||
*/
|
||||
return () => abortController.abort();
|
||||
}, []);
|
||||
|
||||
const fetchAllData = async () => {
|
||||
setTimeout(async () => {
|
||||
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
|
||||
getFoldersApi();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if (isAuthenticated) {
|
||||
try {
|
||||
await getTypes();
|
||||
refreshFlows();
|
||||
const res = await getGlobalVariables();
|
||||
setGlobalVariables(res);
|
||||
checkHasStore();
|
||||
fetchApiData();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch data:", error);
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
if (isAuthenticated) {
|
||||
try {
|
||||
await getTypes();
|
||||
await refreshFlows();
|
||||
const res = await getGlobalVariables();
|
||||
setGlobalVariables(res);
|
||||
checkHasStore();
|
||||
fetchApiData();
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch data:", error);
|
||||
reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -141,7 +165,7 @@ export default function App() {
|
|||
setFetchError(false);
|
||||
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
|
||||
if (isLoading === true && window.location.pathname === "/") {
|
||||
navigate("/flows");
|
||||
navigate("/all");
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
|
@ -168,7 +192,7 @@ export default function App() {
|
|||
></FetchErrorComponent>
|
||||
}
|
||||
|
||||
{isLoading ? (
|
||||
{isLoading || loadingFolders ? (
|
||||
<div className="loading-page-panel">
|
||||
<LoadingComponent remSize={50} />
|
||||
</div>
|
||||
|
|
|
|||
9
src/frontend/src/boilerplate/raw-component/index.tsx
Normal file
9
src/frontend/src/boilerplate/raw-component/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
type RawComponentProps = {};
|
||||
const RawComponent = ({}: RawComponentProps) => {
|
||||
return (
|
||||
<>
|
||||
<div>RawComponent</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default RawComponent;
|
||||
162
src/frontend/src/components/ImageViewer/index.tsx
Normal file
162
src/frontend/src/components/ImageViewer/index.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { saveAs } from "file-saver";
|
||||
import OpenSeadragon from "openseadragon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { IMGViewErrorMSG, IMGViewErrorTitle } from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export default function ImageViewer({ image }) {
|
||||
const viewerRef = useRef(null);
|
||||
const [errorDownloading, setErrordownloading] = useState(false);
|
||||
const setErrorList = useAlertStore((state) => state.setErrorData);
|
||||
const [initialMsg, setInicialMsg] = useState("Please build your flow");
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (viewerRef.current) {
|
||||
// Initialize OpenSeadragon viewer
|
||||
const viewer = OpenSeadragon({
|
||||
element: viewerRef.current,
|
||||
prefixUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/openseadragon/2.4.2/images/", // Optional: Set the path to OpenSeadragon images
|
||||
tileSources: { type: "image", url: image },
|
||||
defaultZoomLevel: 1,
|
||||
maxZoomPixelRatio: 4,
|
||||
showNavigationControl: false,
|
||||
});
|
||||
const zoomInButton = document.getElementById("zoom-in-button");
|
||||
const zoomOutButton = document.getElementById("zoom-out-button");
|
||||
const homeButton = document.getElementById("home-button");
|
||||
const fullPageButton = document.getElementById("full-page-button");
|
||||
|
||||
zoomInButton!.addEventListener("click", () =>
|
||||
viewer.viewport.zoomBy(1.2),
|
||||
);
|
||||
zoomOutButton!.addEventListener("click", () =>
|
||||
viewer.viewport.zoomBy(0.8),
|
||||
);
|
||||
homeButton!.addEventListener("click", () => viewer.viewport.goHome());
|
||||
fullPageButton!.addEventListener("click", () =>
|
||||
viewer.setFullScreen(true),
|
||||
);
|
||||
|
||||
// Optionally, you can set additional viewer options here
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
viewer.destroy();
|
||||
zoomInButton!.removeEventListener("click", () =>
|
||||
viewer.viewport.zoomBy(1.2),
|
||||
);
|
||||
zoomOutButton!.removeEventListener("click", () =>
|
||||
viewer.viewport.zoomBy(0.8),
|
||||
);
|
||||
homeButton!.removeEventListener("click", () =>
|
||||
viewer.viewport.goHome(),
|
||||
);
|
||||
fullPageButton!.removeEventListener("click", () =>
|
||||
viewer.setFullScreen(true),
|
||||
);
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error initializing OpenSeadragon:", error);
|
||||
}
|
||||
}, [image]);
|
||||
|
||||
function download() {
|
||||
const imageUrl = image;
|
||||
// Fetch the image data
|
||||
fetch(imageUrl)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
// Save the image using FileSaver.js
|
||||
saveAs(blob, "image.jpg");
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorList({ title: "There was an error downloading your image" });
|
||||
console.error("Error downloading image:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return image === "" ? (
|
||||
<div className="align-center flex h-full w-full flex-col justify-center gap-5 rounded-md border border-border bg-muted">
|
||||
<div className="align-center flex justify-center gap-2 ">
|
||||
<ForwardedIconComponent name="Image" />
|
||||
{IMGViewErrorTitle}
|
||||
</div>
|
||||
<div className="align-center flex justify-center">
|
||||
<div className="langflow-chat-desc align-center flex justify-center">
|
||||
<div className="langflow-chat-desc-span">{IMGViewErrorMSG}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="align-center my-2 mb-4 flex w-full justify-center">
|
||||
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex w-[50%] items-center justify-center rounded-sm border bg-muted shadow-md transition-all">
|
||||
<button
|
||||
id="zoom-in-button"
|
||||
className="relative inline-flex w-full w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="ZoomIn"
|
||||
className={"h-5 w-5 text-secondary-foreground"}
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<button
|
||||
id="zoom-out-button"
|
||||
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="ZoomOut"
|
||||
className={"h-5 w-5 text-secondary-foreground"}
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<button
|
||||
id="home-button"
|
||||
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="RotateCcw"
|
||||
className={"h-5 w-5 text-secondary-foreground"}
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<button
|
||||
id="full-page-button"
|
||||
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Maximize2"
|
||||
className={"h-5 w-5 text-secondary-foreground"}
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={download}
|
||||
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="ArrowDownToLine"
|
||||
className={"h-5 w-5 text-secondary-foreground"}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="canvas" ref={viewerRef} className={`h-[90%] w-full `} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { useState } from "react";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { AccordionComponentType } from "../../../../types/components";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../../ui/custom-accordion";
|
||||
|
||||
export default function FolderAccordionComponent({
|
||||
trigger,
|
||||
open = [],
|
||||
keyValue,
|
||||
options,
|
||||
}: AccordionComponentType): JSX.Element {
|
||||
const [value, setValue] = useState(
|
||||
open.length === 0 ? "" : getOpenAccordion(),
|
||||
);
|
||||
|
||||
function getOpenAccordion(): string {
|
||||
let value = "";
|
||||
open.forEach((el) => {
|
||||
if (el == trigger) {
|
||||
value = trigger;
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function handleClick(): void {
|
||||
value === "" ? setValue(keyValue!) : setValue("");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion
|
||||
type="single"
|
||||
className="w-full"
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<AccordionItem value={keyValue!} className="">
|
||||
<AccordionTrigger
|
||||
onClick={() => {
|
||||
handleClick();
|
||||
}}
|
||||
className="px-2"
|
||||
>
|
||||
{trigger}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{options!.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex cursor-pointer px-2 py-1 hover:bg-muted-foreground/10"
|
||||
>
|
||||
<IconComponent
|
||||
name={option.icon}
|
||||
className="relative top-[1.5px] mr-2 h-4 w-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{option.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,17 +15,16 @@ export default function AccordionComponent({
|
|||
sideBar,
|
||||
}: AccordionComponentType): JSX.Element {
|
||||
const [value, setValue] = useState(
|
||||
open.length === 0 ? "" : getOpenAccordion()
|
||||
open.length === 0 ? "" : getOpenAccordion(),
|
||||
);
|
||||
|
||||
function getOpenAccordion(): string {
|
||||
let value = "";
|
||||
open.forEach((el) => {
|
||||
if (el == trigger) {
|
||||
value = trigger;
|
||||
if (el == keyValue) {
|
||||
value = keyValue;
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -2,7 +2,8 @@ import { useState } from "react";
|
|||
import { registerGlobalVariable } from "../../controllers/API";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariables";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import InputComponent from "../inputComponent";
|
||||
|
|
@ -10,31 +11,54 @@ import { Button } from "../ui/button";
|
|||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import sortByName from "./utils/sort-by-name";
|
||||
|
||||
//TODO IMPLEMENT FORM LOGIC
|
||||
|
||||
export default function AddNewVariableButton({ children }): JSX.Element {
|
||||
const [key, setKey] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [type, setType] = useState("Generic");
|
||||
const [fields, setFields] = useState<string[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const addGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.addGlobalVariable
|
||||
const componentFields = useTypesStore((state) => state.ComponentFields);
|
||||
const unavaliableFields = new Set(
|
||||
Object.keys(useGlobalVariablesStore((state) => state.unavaliableFields)),
|
||||
);
|
||||
|
||||
const availableFields = () => {
|
||||
const fields = Array.from(componentFields).filter(
|
||||
(field) => !unavaliableFields.has(field),
|
||||
);
|
||||
|
||||
return sortByName(fields);
|
||||
};
|
||||
|
||||
const addGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.addGlobalVariable,
|
||||
);
|
||||
|
||||
function handleSaveVariable() {
|
||||
let data: { name: string; value: string; type?: string } = {
|
||||
let data: {
|
||||
name: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
default_fields?: string[];
|
||||
} = {
|
||||
name: key,
|
||||
type,
|
||||
value,
|
||||
default_fields: fields,
|
||||
};
|
||||
registerGlobalVariable(data)
|
||||
.then((res) => {
|
||||
const { name, id, type } = res.data;
|
||||
addGlobalVariable(name, id, type);
|
||||
addGlobalVariable(name, id, type, fields);
|
||||
setKey("");
|
||||
setValue("");
|
||||
setType("");
|
||||
setFields([]);
|
||||
setOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -79,16 +103,38 @@ export default function AddNewVariableButton({ children }): JSX.Element {
|
|||
password={false}
|
||||
options={["Generic", "Credential"]}
|
||||
placeholder="Choose a type for the variable..."
|
||||
id={"type-global-variables"}
|
||||
></InputComponent>
|
||||
<Label>Value</Label>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Insert a value for the variable..."
|
||||
className="w-full resize-none custom-scroll"
|
||||
/>
|
||||
{type === "Credential" ? (
|
||||
<InputComponent
|
||||
password
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e);
|
||||
}}
|
||||
placeholder="Insert a value for the variable..."
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Insert a value for the variable..."
|
||||
className="w-full resize-none custom-scroll"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Label>Apply To Fields (optional)</Label>
|
||||
<InputComponent
|
||||
setSelectedOptions={(value) => setFields(value)}
|
||||
selectedOptions={fields}
|
||||
options={availableFields()}
|
||||
password={false}
|
||||
placeholder="Choose a field for the variable..."
|
||||
id={"apply-to-fields"}
|
||||
></InputComponent>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export default function sortByName(stringList: string[]): string[] {
|
||||
return stringList.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
12
src/frontend/src/components/arrayReaderComponent/index.tsx
Normal file
12
src/frontend/src/components/arrayReaderComponent/index.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
|
||||
//TODO check array type
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{array.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { storeComponent } from "../../../../types/store";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Card, CardHeader, CardTitle } from "../../../ui/card";
|
||||
|
||||
export default function DragCardComponent({ data }: { data: storeComponent }) {
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
draggable
|
||||
//TODO check color schema
|
||||
className={cn(
|
||||
"group relative flex flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
|
||||
<ForwardedIconComponent
|
||||
className={cn(
|
||||
"visible flex-shrink-0",
|
||||
data.is_component
|
||||
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon",
|
||||
)}
|
||||
name={data.is_component ? "ToyBrick" : "Group"}
|
||||
/>
|
||||
|
||||
<div className="w-full truncate pr-3">{data.name}</div>
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,21 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Control } from "react-hook-form";
|
||||
import { getComponent, postLikeComponent } from "../../controllers/API";
|
||||
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
|
||||
import IOModal from "../../modals/IOModal";
|
||||
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useStoreStore } from "../../stores/storeStore";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { storeComponent } from "../../types/store";
|
||||
import cloneFLowWithParent from "../../utils/storeUtils";
|
||||
import cloneFLowWithParent, {
|
||||
getInputsAndOutputs,
|
||||
} from "../../utils/storeUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
import ShadTooltip from "../shadTooltipComponent";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -18,37 +24,94 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../ui/card";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { FormControl, FormField } from "../ui/form";
|
||||
import Loading from "../ui/loading";
|
||||
import { convertTestName } from "./utils/convert-test-name";
|
||||
import DragCardComponent from "./components/dragCardComponent";
|
||||
|
||||
export default function CollectionCardComponent({
|
||||
data,
|
||||
authorized = true,
|
||||
disabled = false,
|
||||
button,
|
||||
onClick,
|
||||
onDelete,
|
||||
playground,
|
||||
control,
|
||||
is_component,
|
||||
}: {
|
||||
data: storeComponent;
|
||||
authorized?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
button?: JSX.Element;
|
||||
playground?: boolean;
|
||||
onDelete?: () => void;
|
||||
control?: Control<any, any>;
|
||||
is_component?: boolean;
|
||||
}) {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
|
||||
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
|
||||
const isStore = false;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingLike, setLoadingLike] = useState(false);
|
||||
const [liked_by_user, setLiked_by_user] = useState(
|
||||
data?.liked_by_user ?? false
|
||||
data?.liked_by_user ?? false,
|
||||
);
|
||||
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
|
||||
const [downloads_count, setDownloads_count] = useState(
|
||||
data?.downloads_count ?? 0
|
||||
data?.downloads_count ?? 0,
|
||||
);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const setNodes = useFlowStore((state) => state.setNodes);
|
||||
const setEdges = useFlowStore((state) => state.setEdges);
|
||||
const [openPlayground, setOpenPlayground] = useState(false);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const setCurrentFlowId = useFlowsManagerStore(
|
||||
(state) => state.setCurrentFlowId,
|
||||
);
|
||||
const [loadingPlayground, setLoadingPlayground] = useState(false);
|
||||
|
||||
const selectedFlowsComponentsCards = useFlowsManagerStore(
|
||||
(state) => state.selectedFlowsComponentsCards,
|
||||
);
|
||||
|
||||
const name = data.is_component ? "Component" : "Flow";
|
||||
|
||||
async function getFlowData() {
|
||||
const res = await getComponent(data.id);
|
||||
const newFlow = cloneFLowWithParent(res, res.id, data.is_component, true);
|
||||
return newFlow;
|
||||
}
|
||||
|
||||
function hasPlayground(flow?: FlowType) {
|
||||
if (!flow) {
|
||||
return false;
|
||||
}
|
||||
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
|
||||
return inputs.length > 0 || outputs.length > 0;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFlowId && playground) {
|
||||
if (openPlayground) {
|
||||
setNodes(currentFlow?.data?.nodes ?? [], true);
|
||||
setEdges(currentFlow?.data?.edges ?? [], true);
|
||||
} else {
|
||||
setNodes([], true);
|
||||
setEdges([], true);
|
||||
}
|
||||
cleanFlowPool();
|
||||
}
|
||||
}, [openPlayground]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setLiked_by_user(data?.liked_by_user ?? false);
|
||||
|
|
@ -127,227 +190,403 @@ export default function CollectionCardComponent({
|
|||
}
|
||||
}
|
||||
|
||||
const isSelectedCard =
|
||||
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
|
||||
|
||||
function onDragStart(event: React.DragEvent<any>) {
|
||||
let image: JSX.Element = <DragCardComponent data={data} />; // <== whatever you want here
|
||||
|
||||
var ghost = document.createElement("div");
|
||||
ghost.style.transform = "translate(-10000px, -10000px)";
|
||||
ghost.style.position = "absolute";
|
||||
document.body.appendChild(ghost);
|
||||
event.dataTransfer.setDragImage(ghost, 0, 0);
|
||||
const root = createRoot(ghost);
|
||||
root.render(image);
|
||||
const flow = getFlowById(data.id);
|
||||
if (flow) {
|
||||
event.dataTransfer.setData("flow", JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:shadow-md",
|
||||
disabled ? "pointer-events-none opacity-50" : ""
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"flex-shrink-0",
|
||||
data.is_component
|
||||
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon"
|
||||
)}
|
||||
name={data.is_component ? "ToyBrick" : "Group"}
|
||||
/>
|
||||
<ShadTooltip content={data.name}>
|
||||
<div className="w-full truncate">{data.name}</div>
|
||||
</ShadTooltip>
|
||||
{data?.metadata !== undefined && (
|
||||
<div className="flex gap-3">
|
||||
{data.private && (
|
||||
<ShadTooltip content="Private">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="Lock" className="h-4 w-4" />
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
<>
|
||||
<Card
|
||||
onDragStart={onDragStart}
|
||||
draggable
|
||||
data-testid={`card-${convertTestName(data.name)}`}
|
||||
//TODO check color schema
|
||||
className={cn(
|
||||
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
|
||||
disabled ? "pointer-events-none opacity-50" : "",
|
||||
onClick ? "cursor-pointer" : "",
|
||||
isSelectedCard ? "border border-selected" : "",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"visible flex-shrink-0",
|
||||
data.is_component
|
||||
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon",
|
||||
)}
|
||||
{!data.is_component && (
|
||||
<ShadTooltip content="Components">
|
||||
name={data.is_component ? "ToyBrick" : "Group"}
|
||||
/>
|
||||
|
||||
<ShadTooltip content={data.name}>
|
||||
<div className="w-full truncate pr-3">{data.name}</div>
|
||||
</ShadTooltip>
|
||||
{data?.metadata !== undefined && (
|
||||
<div className="flex items-center gap-3">
|
||||
{data.private && (
|
||||
<ShadTooltip content="Private">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="Lock" className="h-4 w-4" />
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
{!data.is_component && (
|
||||
<ShadTooltip content="Components">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="ToyBrick" className="h-4 w-4" />
|
||||
<span data-testid={`total-${data.name}`}>
|
||||
{data?.metadata?.total ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
<ShadTooltip content="Likes">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="ToyBrick" className="h-4 w-4" />
|
||||
<span data-testid={`total-${data.name}`}>
|
||||
{data?.metadata?.total ?? 0}
|
||||
<IconComponent
|
||||
name="Heart"
|
||||
className={cn("h-4 w-4 ")}
|
||||
/>
|
||||
<span data-testid={`likes-${data.name}`}>
|
||||
{likes_count ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
<ShadTooltip content="Likes">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
|
||||
<span data-testid={`likes-${data.name}`}>
|
||||
{likes_count ?? 0}
|
||||
<ShadTooltip content="Downloads">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span data-testid={`downloads-${data.name}`}>
|
||||
{downloads_count ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
<ShadTooltip content="Downloads">
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<IconComponent name="DownloadCloud" className="h-4 w-4" />
|
||||
<span data-testid={`downloads-${data.name}`}>
|
||||
{downloads_count ?? 0}
|
||||
</span>
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onDelete && data?.metadata === undefined && (
|
||||
<DeleteConfirmationModal
|
||||
onConfirm={() => {
|
||||
onDelete();
|
||||
{control && (
|
||||
<div
|
||||
className="flex"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`${data.id}`}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
aria-label="checkbox-component"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="h-5 w-5 border border-ring data-[state=checked]:border-selected data-[state=checked]:bg-selected"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{data.user_created && data.user_created.username && (
|
||||
<span className="text-sm text-primary">
|
||||
by <b>{data.user_created.username}</b>
|
||||
{data.last_tested_version && (
|
||||
<>
|
||||
{" "}
|
||||
|{" "}
|
||||
<span className="text-xs">
|
||||
{" "}
|
||||
⛓︎ v{data.last_tested_version}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex w-full flex-1 flex-wrap gap-2">
|
||||
{/* {data.tags &&
|
||||
data.tags.length > 0 &&
|
||||
data.tags.map((tag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="xq"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardDescription className="pb-2 pt-2">
|
||||
<div className="truncate-doubleline">{data.description}</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardFooter>
|
||||
<div className="z-50 flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-full flex-wrap items-end justify-end gap-2">
|
||||
{playground && data?.metadata !== undefined ? (
|
||||
<Button
|
||||
disabled={loadingPlayground || !authorized}
|
||||
key={data.id}
|
||||
tabIndex={-1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="z-50 gap-2 whitespace-nowrap"
|
||||
data-testid={"playground-flow-button-" + data.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setLoadingPlayground(true);
|
||||
const flow = getFlowById(data.id);
|
||||
if (flow) {
|
||||
if (!hasPlayground(flow)) {
|
||||
setErrorData({
|
||||
title: "Error",
|
||||
list: ["This flow doesn't have a playground."],
|
||||
});
|
||||
setLoadingPlayground(false);
|
||||
return;
|
||||
}
|
||||
setCurrentFlowId(data.id);
|
||||
setOpenPlayground(true);
|
||||
setLoadingPlayground(false);
|
||||
} else {
|
||||
getFlowData().then((res) => {
|
||||
if (!hasPlayground(res)) {
|
||||
setErrorData({
|
||||
title: "Error",
|
||||
list: ["This flow doesn't have a playground."],
|
||||
});
|
||||
setLoadingPlayground(false);
|
||||
return;
|
||||
}
|
||||
setCurrentFlow(res);
|
||||
setOpenPlayground(true);
|
||||
setLoadingPlayground(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
|
||||
/>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
{data.user_created && data.user_created.username && (
|
||||
<span className="text-sm text-primary">
|
||||
by <b>{data.user_created.username}</b>
|
||||
{data.last_tested_version && (
|
||||
<>
|
||||
{" "}
|
||||
|{" "}
|
||||
<span className="text-xs">
|
||||
{" "}
|
||||
⛓︎ v{data.last_tested_version}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<CardDescription className="pb-2 pt-2">
|
||||
<div className="truncate-doubleline">{data.description}</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-full flex-wrap items-end justify-between gap-2">
|
||||
<div className="flex w-full flex-1 flex-wrap gap-2">
|
||||
{data.tags &&
|
||||
data.tags.length > 0 &&
|
||||
data.tags.map((tag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="xq"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{data.liked_by_count != undefined && (
|
||||
<div className="flex gap-0.5">
|
||||
{onDelete && data?.metadata !== undefined ? (
|
||||
<ShadTooltip
|
||||
content={
|
||||
authorized ? "Delete" : "Please review your API key."
|
||||
}
|
||||
>
|
||||
<DeleteConfirmationModal
|
||||
onConfirm={() => {
|
||||
onDelete();
|
||||
}}
|
||||
{!loadingPlayground ? (
|
||||
<IconComponent
|
||||
name="BotMessageSquareIcon"
|
||||
className="h-4 w-4 select-none"
|
||||
/>
|
||||
) : (
|
||||
<Loading className="h-4 w-4 text-medium-indigo" />
|
||||
)}
|
||||
Playground
|
||||
</Button>
|
||||
) : undefined}
|
||||
{data.liked_by_count != undefined && (
|
||||
<div className="flex gap-0.5">
|
||||
{onDelete && data?.metadata !== undefined ? (
|
||||
<ShadTooltip
|
||||
content={
|
||||
authorized ? "Delete" : "Please review your API key."
|
||||
}
|
||||
>
|
||||
<DeleteConfirmationModal onConfirm={onDelete}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={
|
||||
"whitespace-nowrap" +
|
||||
(!authorized ? " cursor-not-allowed" : "")
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
data-testid={`delete-${convertTestName(data.name)}`}
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-5 w-5",
|
||||
!authorized ? " text-ring" : "",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</DeleteConfirmationModal>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
<ShadTooltip
|
||||
content={
|
||||
authorized ? "Like" : "Please review your API key."
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={loadingLike}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={
|
||||
"whitespace-nowrap" +
|
||||
(!authorized ? " cursor-not-allowed" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
if (!authorized) {
|
||||
return;
|
||||
}
|
||||
handleLike();
|
||||
}}
|
||||
data-testid={`like-${data.name}`}
|
||||
>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
name="Heart"
|
||||
className={cn(
|
||||
"h-5 w-5",
|
||||
!authorized ? " text-ring" : ""
|
||||
liked_by_user
|
||||
? "fill-destructive stroke-destructive"
|
||||
: "",
|
||||
!authorized ? " text-ring" : "",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</DeleteConfirmationModal>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
</ShadTooltip>
|
||||
)}
|
||||
<ShadTooltip
|
||||
content={
|
||||
authorized ? "Like" : "Please review your API key."
|
||||
authorized
|
||||
? isStore
|
||||
? "Download"
|
||||
: "Install Locally"
|
||||
: "Please review your API key."
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={loadingLike}
|
||||
disabled={loading}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={
|
||||
"whitespace-nowrap" +
|
||||
(!authorized ? " cursor-not-allowed" : "")
|
||||
(!authorized ? " cursor-not-allowed" : "") +
|
||||
(!loading ? " p-0.5" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
if (!authorized) {
|
||||
if (loading || !authorized) {
|
||||
return;
|
||||
}
|
||||
handleLike();
|
||||
handleInstall();
|
||||
}}
|
||||
data-testid={`like-${data.name}`}
|
||||
data-testid={`install-${data.name}`}
|
||||
>
|
||||
<IconComponent
|
||||
name="Heart"
|
||||
name={
|
||||
loading ? "Loader2" : isStore ? "Download" : "Plus"
|
||||
}
|
||||
className={cn(
|
||||
"h-5 w-5",
|
||||
liked_by_user
|
||||
? "fill-destructive stroke-destructive"
|
||||
: "",
|
||||
!authorized ? " text-ring" : ""
|
||||
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
|
||||
!authorized ? " text-ring" : "",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
<ShadTooltip
|
||||
content={
|
||||
authorized
|
||||
? isStore
|
||||
? "Download"
|
||||
: "Install Locally"
|
||||
: "Please review your API key."
|
||||
}
|
||||
>
|
||||
<Button
|
||||
disabled={loading}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={
|
||||
"whitespace-nowrap" +
|
||||
(!authorized ? " cursor-not-allowed" : "") +
|
||||
(!loading ? " p-0.5" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
if (loading || !authorized) {
|
||||
</div>
|
||||
)}
|
||||
{playground && data?.metadata === undefined && (
|
||||
<Button
|
||||
disabled={loadingPlayground || !authorized}
|
||||
key={data.id}
|
||||
tabIndex={-1}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="gap-2 whitespace-nowrap bg-muted"
|
||||
data-testid={"playground-flow-button-" + data.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setLoadingPlayground(true);
|
||||
const flow = getFlowById(data.id);
|
||||
if (flow) {
|
||||
if (!hasPlayground(flow)) {
|
||||
setErrorData({
|
||||
title: "Error",
|
||||
list: ["This flow doesn't have a playground."],
|
||||
});
|
||||
setLoadingPlayground(false);
|
||||
return;
|
||||
}
|
||||
handleInstall();
|
||||
}}
|
||||
data-testid={`install-${data.name}`}
|
||||
>
|
||||
setCurrentFlowId(data.id);
|
||||
setOpenPlayground(true);
|
||||
setLoadingPlayground(false);
|
||||
} else {
|
||||
getFlowData().then((res) => {
|
||||
if (!hasPlayground(res)) {
|
||||
setErrorData({
|
||||
title: "Error",
|
||||
list: ["This flow doesn't have a playground."],
|
||||
});
|
||||
setLoadingPlayground(false);
|
||||
return;
|
||||
}
|
||||
setCurrentFlow(res);
|
||||
setOpenPlayground(true);
|
||||
setLoadingPlayground(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!loadingPlayground ? (
|
||||
<IconComponent
|
||||
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
|
||||
className={cn(
|
||||
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
|
||||
!authorized ? " text-ring" : ""
|
||||
)}
|
||||
name="BotMessageSquareIcon"
|
||||
className="h-4 w-4 select-none"
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
{button && button}
|
||||
) : (
|
||||
<Loading className="h-4 w-4 text-medium-indigo" />
|
||||
)}
|
||||
Playground
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{openPlayground && (
|
||||
<IOModal
|
||||
cleanOnClose={true}
|
||||
open={openPlayground}
|
||||
setOpen={setOpenPlayground}
|
||||
>
|
||||
<></>
|
||||
</IOModal>
|
||||
)}
|
||||
{openDelete && (
|
||||
<DeleteConfirmationModal
|
||||
open={openDelete}
|
||||
setOpen={setOpenDelete}
|
||||
onConfirm={() => {
|
||||
if (onDelete) onDelete();
|
||||
}}
|
||||
description={` ${is_component ? "component" : "flow"}`}
|
||||
>
|
||||
<></>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export function convertTestName(name: string): string {
|
||||
return name.replace(/ /g, "-").toLowerCase();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
export default function CardsWrapComponent({
|
||||
onFileDrop,
|
||||
|
|
@ -11,6 +12,23 @@ export default function CardsWrapComponent({
|
|||
dragMessage?: string;
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
useEffect(() => {
|
||||
// Function to handle visibility change
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
// Reset hover state or perform any necessary actions when the tab becomes visible again
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener for visibility change
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const dragOver = (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -43,12 +61,12 @@ export default function CardsWrapComponent({
|
|||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
onDrop={onDrop}
|
||||
className={
|
||||
"h-full w-full " +
|
||||
(isDragging
|
||||
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
|
||||
: "")
|
||||
}
|
||||
className={cn(
|
||||
"h-full w-full",
|
||||
isDragging
|
||||
? "mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{isDragging ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ApiModal from "../../modals/ApiModal";
|
||||
import IOModal from "../../modals/IOModal";
|
||||
import ApiModal from "../../modals/apiModal/views";
|
||||
import ShareModal from "../../modals/shareModal";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
|
|
@ -15,8 +15,8 @@ export default function FlowToolbar(): JSX.Element {
|
|||
const hasIO = useFlowStore((state) => state.hasIO);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -50,7 +50,7 @@ export default function FlowToolbar(): JSX.Element {
|
|||
"relative inline-flex h-full w-full items-center justify-center gap-[4px] bg-muted px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-background hover:bg-hover ",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? " button-disable text-muted-foreground "
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
|
|
@ -59,14 +59,14 @@ export default function FlowToolbar(): JSX.Element {
|
|||
"-m-0.5 -ml-1 h-6 w-6",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "extra-side-bar-save-disable"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
/>
|
||||
Share
|
||||
</button>
|
||||
</ShareModal>
|
||||
),
|
||||
[hasApiKey, validApiKey, currentFlow, hasStore]
|
||||
[hasApiKey, validApiKey, currentFlow, hasStore],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -87,15 +87,15 @@ export default function FlowToolbar(): JSX.Element {
|
|||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex h-full w-full gap-1 rounded-sm text-medium-indigo transition-all">
|
||||
<div className="flex h-full w-full gap-1 rounded-sm transition-all">
|
||||
{hasIO ? (
|
||||
<IOModal open={open} setOpen={setOpen} disable={!hasIO}>
|
||||
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-medium-indigo transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
|
||||
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold transition-all duration-500 ease-in-out hover:bg-hover">
|
||||
<ForwardedIconComponent
|
||||
name="Zap"
|
||||
className={"message-button-icon h-5 w-5 transition-all"}
|
||||
name="BotMessageSquareIcon"
|
||||
className={" h-5 w-5 transition-all"}
|
||||
/>
|
||||
Run
|
||||
Playground
|
||||
</div>
|
||||
</IOModal>
|
||||
) : (
|
||||
|
|
@ -103,12 +103,10 @@ export default function FlowToolbar(): JSX.Element {
|
|||
className={`relative inline-flex w-full cursor-not-allowed items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-muted-foreground transition-all duration-150 ease-in-out ease-in-out`}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Zap"
|
||||
className={
|
||||
"message-button-icon h-5 w-5 fill-muted-foreground stroke-muted-foreground transition-all"
|
||||
}
|
||||
name="BotMessageSquareIcon"
|
||||
className={" h-5 w-5 transition-all"}
|
||||
/>
|
||||
Run
|
||||
Playground
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -120,7 +118,7 @@ export default function FlowToolbar(): JSX.Element {
|
|||
<ApiModal flow={currentFlow}>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-hover"
|
||||
"relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-hover",
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ export default function CodeAreaComponent({
|
|||
setOpen,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
typeof value == "string" ? value : JSON.stringify(value),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (disabled && myValue !== "") {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { cloneDeep } from "lodash";
|
|||
import { useEffect, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import AccordionComponent from "../../components/AccordionComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
|
|
@ -27,6 +26,8 @@ import {
|
|||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
|
||||
import ExportModal from "../../modals/exportModal";
|
||||
import { Case } from "../../shared/components/caseComponent";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { codeTabsPropsType } from "../../types/components";
|
||||
|
|
@ -36,11 +37,14 @@ import {
|
|||
hasDuplicateKeys,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import AccordionComponent from "../accordionComponent";
|
||||
import DictComponent from "../dictComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import InputGlobalComponent from "../inputGlobalComponent";
|
||||
import InputComponent from "../inputComponent";
|
||||
import KeypairListComponent from "../keypairListComponent";
|
||||
import ShadTooltip from "../shadTooltipComponent";
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
export default function CodeTabsComponent({
|
||||
flow,
|
||||
|
|
@ -49,14 +53,14 @@ export default function CodeTabsComponent({
|
|||
setActiveTab,
|
||||
isMessage,
|
||||
tweaks,
|
||||
setActiveTweaks,
|
||||
activeTweaks,
|
||||
allowExport = false,
|
||||
}: codeTabsPropsType) {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
|
||||
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -85,6 +89,10 @@ export default function CodeTabsComponent({
|
|||
});
|
||||
};
|
||||
|
||||
const type = (node, templateParam) => {
|
||||
return node.data.node.template[templateParam].type;
|
||||
};
|
||||
|
||||
const downloadAsFile = () => {
|
||||
const fileExtension = tabs[activeTab].language || ".txt";
|
||||
const suggestedFileName = `${"generated-code."}${fileExtension}`;
|
||||
|
|
@ -107,21 +115,6 @@ export default function CodeTabsComponent({
|
|||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
function openAccordions() {
|
||||
let accordionsToOpen: string[] = [];
|
||||
tweaks?.tweak!.current.forEach((el) => {
|
||||
Object.keys(el).forEach((key) => {
|
||||
if (Object.keys(el[key]).length > 0) {
|
||||
accordionsToOpen.push(key);
|
||||
setOpenAccordion(accordionsToOpen);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (accordionsToOpen.length == 0) {
|
||||
setOpenAccordion([]);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
|
|
@ -132,9 +125,6 @@ export default function CodeTabsComponent({
|
|||
}
|
||||
onValueChange={(value) => {
|
||||
setActiveTab(value);
|
||||
if (value === "3") {
|
||||
openAccordions();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="api-modal-tablist-div">
|
||||
|
|
@ -155,27 +145,68 @@ export default function CodeTabsComponent({
|
|||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
{Number(activeTab) < 4 && (
|
||||
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
|
||||
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
|
||||
{tweaks && (
|
||||
<div
|
||||
className={
|
||||
Number(activeTab) > 2
|
||||
? "hidden"
|
||||
: "relative top-[2.5px] flex gap-2"
|
||||
}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
) : (
|
||||
<IconComponent name="Clipboard" className="h-4 w-4" />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy Code"}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={downloadAsFile}
|
||||
>
|
||||
<IconComponent name="Download" className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Switch
|
||||
style={{
|
||||
transform: `scaleX(${0.7}) scaleY(${0.7})`,
|
||||
}}
|
||||
id="tweaks-switch"
|
||||
onCheckedChange={setActiveTweaks}
|
||||
autoFocus={false}
|
||||
/>
|
||||
<Label
|
||||
className={
|
||||
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " +
|
||||
(activeTweaks
|
||||
? "font-bold text-black dark:text-white"
|
||||
: "font-medium")
|
||||
}
|
||||
htmlFor="tweaks-switch"
|
||||
>
|
||||
Tweaks
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{allowExport && (
|
||||
<ExportModal>
|
||||
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300">
|
||||
<IconComponent name="FileDown" className="h-4 w-4" />
|
||||
Export Flow
|
||||
</div>
|
||||
</ExportModal>
|
||||
)}
|
||||
|
||||
{Number(activeTab) < 4 && (
|
||||
<>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
) : (
|
||||
<IconComponent name="Clipboard" className="h-4 w-4" />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy Code"}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={downloadAsFile}
|
||||
>
|
||||
<IconComponent name="Download" className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tabs.map((tab, idx) => (
|
||||
|
|
@ -205,14 +236,12 @@ export default function CodeTabsComponent({
|
|||
<div className="api-modal-according-display">
|
||||
<div
|
||||
className={classNames(
|
||||
"h-[70vh] w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll"
|
||||
"h-[70vh] w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll",
|
||||
)}
|
||||
>
|
||||
{data?.map((node: any, i) => (
|
||||
<div className="px-3" key={i}>
|
||||
{tweaks?.tweaksList!.current.includes(
|
||||
node["data"]["id"]
|
||||
) && (
|
||||
{tweaks?.tweaksList!.includes(node["data"]["id"]) && (
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<ShadTooltip
|
||||
|
|
@ -223,7 +252,6 @@ export default function CodeTabsComponent({
|
|||
<div>{node["data"]["node"]["display_name"]}</div>
|
||||
</ShadTooltip>
|
||||
}
|
||||
open={openAccordion}
|
||||
keyValue={node["data"]["id"]}
|
||||
>
|
||||
<div className="api-modal-table-arrangement">
|
||||
|
|
@ -247,8 +275,8 @@ export default function CodeTabsComponent({
|
|||
.show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
node.data.node.template[templateField]
|
||||
.type
|
||||
)
|
||||
.type,
|
||||
),
|
||||
)
|
||||
.map((templateField, indx) => {
|
||||
return (
|
||||
|
|
@ -258,22 +286,71 @@ export default function CodeTabsComponent({
|
|||
</TableCell>
|
||||
<TableCell className="p-0 text-xs text-foreground">
|
||||
<div className="m-auto w-[250px]">
|
||||
{node.data.node.template[
|
||||
templateField
|
||||
].type === "str" &&
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].options ? (
|
||||
<div className="mx-auto">
|
||||
{node.data.node.template[
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"str" &&
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].list ? (
|
||||
<InputListComponent
|
||||
componentName={
|
||||
].options
|
||||
}
|
||||
>
|
||||
<Case
|
||||
condition={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]?.list
|
||||
}
|
||||
>
|
||||
<InputListComponent
|
||||
componentName={templateField}
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
}
|
||||
editNode={true}
|
||||
].value ||
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value === ""
|
||||
? [""]
|
||||
: node.data.node.template[
|
||||
templateField
|
||||
].value
|
||||
}
|
||||
onChange={(target) => {
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].multiline
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<TextAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
|
|
@ -281,7 +358,7 @@ export default function CodeTabsComponent({
|
|||
node.data.node.template[
|
||||
templateField
|
||||
].value === ""
|
||||
? [""]
|
||||
? ""
|
||||
: node.data.node
|
||||
.template[
|
||||
templateField
|
||||
|
|
@ -298,117 +375,77 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : node.data.node.template[
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].multiline ? (
|
||||
<div>
|
||||
<TextAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={
|
||||
!node.data.node
|
||||
.template[
|
||||
].multiline &&
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].list
|
||||
}
|
||||
>
|
||||
<InputComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
password={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].password ?? false
|
||||
}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].value ||
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value === ""
|
||||
? ""
|
||||
: node.data.node.template[
|
||||
templateField
|
||||
].value ||
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value === ""
|
||||
? ""
|
||||
: node.data.node
|
||||
.template[
|
||||
templateField
|
||||
].value
|
||||
}
|
||||
onChange={(target) => {
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node
|
||||
.template[
|
||||
templateField
|
||||
]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<InputGlobalComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
onChange={(target) => {
|
||||
if (node.data) {
|
||||
setNode(
|
||||
node.data.id,
|
||||
(oldNode) => {
|
||||
let newNode =
|
||||
cloneDeep(
|
||||
oldNode
|
||||
);
|
||||
].value
|
||||
}
|
||||
onChange={(target) => {
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Case>
|
||||
</Case>
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
|
||||
newNode.data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
);
|
||||
}
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
);
|
||||
}}
|
||||
setDb={(value) => {
|
||||
setNode(
|
||||
node.data.id,
|
||||
(oldNode) => {
|
||||
let newNode =
|
||||
cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template[
|
||||
templateField
|
||||
].load_from_db =
|
||||
value;
|
||||
return newNode;
|
||||
}
|
||||
);
|
||||
}}
|
||||
name={templateField}
|
||||
data={node.data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "bool" ? (
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"bool"
|
||||
}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
|
|
@ -428,21 +465,26 @@ export default function CodeTabsComponent({
|
|||
].value = e;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
e,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "file" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"file"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
|
|
@ -459,7 +501,7 @@ export default function CodeTabsComponent({
|
|||
].fileTypes
|
||||
}
|
||||
onFileChange={(
|
||||
value: any
|
||||
value: any,
|
||||
) => {
|
||||
node.data.node.template[
|
||||
templateField
|
||||
|
|
@ -467,9 +509,14 @@ export default function CodeTabsComponent({
|
|||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "float" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"float"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
disabled={false}
|
||||
|
|
@ -502,22 +549,27 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "str" &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].options ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"str" &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].options
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
editNode={true}
|
||||
|
|
@ -537,12 +589,12 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
value={
|
||||
|
|
@ -559,9 +611,14 @@ export default function CodeTabsComponent({
|
|||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "int" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"int"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
disabled={false}
|
||||
|
|
@ -594,19 +651,24 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "prompt" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"prompt"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={true}
|
||||
|
|
@ -635,19 +697,24 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "code" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"code"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
disabled={false}
|
||||
|
|
@ -676,19 +743,24 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "dict" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"dict"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto overflow-auto custom-scroll">
|
||||
<KeypairListComponent
|
||||
disabled={false}
|
||||
|
|
@ -705,7 +777,11 @@ export default function CodeTabsComponent({
|
|||
node.data.node!
|
||||
.template[
|
||||
templateField
|
||||
].value
|
||||
].value,
|
||||
type(
|
||||
node,
|
||||
templateField,
|
||||
),
|
||||
)
|
||||
}
|
||||
duplicateKey={
|
||||
|
|
@ -714,15 +790,15 @@ export default function CodeTabsComponent({
|
|||
onChange={(target) => {
|
||||
const valueToNumbers =
|
||||
convertValuesToNumbers(
|
||||
target
|
||||
target,
|
||||
);
|
||||
node.data.node!.template[
|
||||
templateField
|
||||
].value = valueToNumbers;
|
||||
setErrorDuplicateKey(
|
||||
hasDuplicateKeys(
|
||||
valueToNumbers
|
||||
)
|
||||
valueToNumbers,
|
||||
),
|
||||
);
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
|
|
@ -734,24 +810,29 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
isList={
|
||||
node.data.node!.template[
|
||||
templateField
|
||||
].list ?? false
|
||||
]?.list ?? false
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "NestedDict" ? (
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"NestedDict"
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<DictComponent
|
||||
disabled={false}
|
||||
|
|
@ -759,9 +840,9 @@ export default function CodeTabsComponent({
|
|||
value={
|
||||
node.data.node!.template[
|
||||
templateField
|
||||
].value.toString() === "{}"
|
||||
].value?.toString() === "{}"
|
||||
? {
|
||||
yourkey: "value",
|
||||
// yourkey: "value",
|
||||
}
|
||||
: node.data.node!
|
||||
.template[
|
||||
|
|
@ -779,23 +860,26 @@ export default function CodeTabsComponent({
|
|||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject!(
|
||||
tweaks?.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
templateField
|
||||
]
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.data.node.template[
|
||||
templateField
|
||||
].type === "Any" ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="hidden"></div>
|
||||
)}
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(node, templateField) ===
|
||||
"Any"
|
||||
}
|
||||
>
|
||||
<>-</>
|
||||
</Case>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -807,7 +891,7 @@ export default function CodeTabsComponent({
|
|||
</AccordionComponent>
|
||||
)}
|
||||
|
||||
{tweaks?.tweaksList!.current.length === 0 && (
|
||||
{tweaks?.tweaksList!.length === 0 && (
|
||||
<>
|
||||
<div className="pt-3">
|
||||
No tweaks are available for this flow.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
export const convertCSVToData = (csvFile, csvSeparator: string) => {
|
||||
const lines = csvFile.data.trim().split("\n");
|
||||
const headers = lines[0].trim().split(csvSeparator);
|
||||
|
||||
const initialRowData: any = [];
|
||||
const initialColDefs = headers.map((header) => ({
|
||||
field: header.trim(),
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
height: "100%",
|
||||
}));
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const data = lines[i].trim().split(csvSeparator);
|
||||
const rowDataEntry: any = {};
|
||||
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
const value = isNaN(data[j]) ? data[j] : parseFloat(data[j]);
|
||||
rowDataEntry[headers[j].trim()] = value;
|
||||
}
|
||||
|
||||
initialRowData.push(rowDataEntry);
|
||||
}
|
||||
|
||||
return { rowData: initialRowData, colDefs: initialColDefs };
|
||||
};
|
||||
135
src/frontend/src/components/csvOutputComponent/index.tsx
Normal file
135
src/frontend/src/components/csvOutputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CSVError,
|
||||
CSVNoDataError,
|
||||
CSVViewErrorTitle,
|
||||
} from "../../constants/constants";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import { FlowPoolObjectType } from "../../types/chat";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import TableComponent from "../tableComponent";
|
||||
import Loading from "../ui/loading";
|
||||
import { convertCSVToData } from "./helpers/convert-data-function";
|
||||
|
||||
function CsvOutputComponent({
|
||||
csvNode,
|
||||
flowPool,
|
||||
}: {
|
||||
csvNode: NodeType;
|
||||
flowPool: FlowPoolObjectType;
|
||||
}) {
|
||||
const csvNodeArtifacts = flowPool?.data?.artifacts?.repr;
|
||||
const jsonString = csvNodeArtifacts?.replace(/'/g, '"');
|
||||
let file = null;
|
||||
try {
|
||||
file = JSON?.parse(jsonString) || "";
|
||||
} catch (e) {
|
||||
console.log("Error parsing JSON");
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
|
||||
<div className="align-center flex w-full justify-center gap-2">
|
||||
<ForwardedIconComponent name="Table" />
|
||||
{CSVViewErrorTitle}
|
||||
</div>
|
||||
<div className="align-center flex w-full justify-center">
|
||||
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
|
||||
<div className="langflow-chat-desc-span">{CSVError}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const separator = csvNode?.data?.node?.template?.separator?.value || ",";
|
||||
|
||||
const dark = useDarkStore.getState().dark;
|
||||
|
||||
const [rowData, setRowData] = useState([]);
|
||||
const [colDefs, setColDefs] = useState([]);
|
||||
|
||||
const [status, setStatus] = useState("loading");
|
||||
const defaultColDef = useMemo(() => {
|
||||
return {
|
||||
width: 200,
|
||||
editable: true,
|
||||
filter: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setStatus("loading");
|
||||
if (file) {
|
||||
const { rowData: data, colDefs: columns } = convertCSVToData(
|
||||
file,
|
||||
separator,
|
||||
);
|
||||
setRowData(data);
|
||||
setColDefs(columns);
|
||||
|
||||
setTimeout(() => {
|
||||
setStatus("loaded");
|
||||
}, 1000);
|
||||
} else {
|
||||
setStatus("nodata");
|
||||
}
|
||||
}, [separator]);
|
||||
|
||||
return (
|
||||
<div className=" h-full rounded-md border bg-muted">
|
||||
{status === "nodata" && (
|
||||
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
|
||||
<div className="align-center flex w-full justify-center gap-2">
|
||||
<ForwardedIconComponent name="Table" />
|
||||
{CSVViewErrorTitle}
|
||||
</div>
|
||||
<div className="align-center flex w-full justify-center">
|
||||
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
|
||||
<div className="langflow-chat-desc-span">{CSVNoDataError}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
|
||||
<div className="align-center flex w-full justify-center gap-2">
|
||||
<ForwardedIconComponent name="Table" />
|
||||
{CSVViewErrorTitle}
|
||||
</div>
|
||||
<div className="align-center flex w-full justify-center">
|
||||
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
|
||||
<div className="langflow-chat-desc-span">{CSVError}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "loaded" && (
|
||||
<div
|
||||
className={`${dark ? "ag-theme-balham-dark" : "ag-theme-balham"}`}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
>
|
||||
<TableComponent
|
||||
rowData={rowData}
|
||||
columnDefs={colDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
scrollbarWidth={8}
|
||||
overlayNoRowsTemplate="No data available"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{status === "loading" && (
|
||||
<div className=" flex h-full w-full items-center justify-center align-middle">
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CsvOutputComponent;
|
||||
21
src/frontend/src/components/dateReaderComponent/index.tsx
Normal file
21
src/frontend/src/components/dateReaderComponent/index.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export default function DateReader({
|
||||
date: dateString,
|
||||
}: {
|
||||
date: string;
|
||||
}): JSX.Element {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-indexed in JavaScript
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
const hours = date.getHours();
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
const ampm = hours >= 12 ? "PM" : "AM";
|
||||
const hours12 = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours; // Convert to 12-hour format
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours12}:${minutes} ${ampm}`;
|
||||
|
||||
return <span>{formattedDate}</span>;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import { useRef, useState } from "react";
|
||||
import { DropDownComponentType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
|
@ -13,6 +14,7 @@ import {
|
|||
} from "../ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverContentWithoutPortal,
|
||||
PopoverTrigger,
|
||||
} from "../ui/popover";
|
||||
|
|
@ -25,50 +27,65 @@ export default function Dropdown({
|
|||
onSelect,
|
||||
editNode = false,
|
||||
id = "",
|
||||
children,
|
||||
}: DropDownComponentType): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(children ? true : false);
|
||||
|
||||
const refButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const PopoverContentDropdown = children
|
||||
? PopoverContent
|
||||
: PopoverContentWithoutPortal;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.keys(options)?.length > 0 ? (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
ref={refButton}
|
||||
aria-expanded={open}
|
||||
data-testid={`${id ?? ""}`}
|
||||
className={cn(
|
||||
editNode
|
||||
? "dropdown-component-outline"
|
||||
: "dropdown-component-false-outline",
|
||||
"w-full justify-between font-normal",
|
||||
editNode ? "input-edit-node" : "py-2"
|
||||
)}
|
||||
>
|
||||
<span data-testid={`value-dropdown-` + id}>
|
||||
{value &&
|
||||
value !== "" &&
|
||||
options.find((option) => option === value)
|
||||
? options.find((option) => option === value)
|
||||
: "Choose an option..."}
|
||||
</span>
|
||||
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
|
||||
{children ? (
|
||||
<PopoverAnchor>{children}</PopoverAnchor>
|
||||
) : (
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
ref={refButton}
|
||||
aria-expanded={open}
|
||||
data-testid={`${id ?? ""}`}
|
||||
className={cn(
|
||||
editNode
|
||||
? "dropdown-component-outline"
|
||||
: "dropdown-component-false-outline",
|
||||
"w-full justify-between font-normal",
|
||||
editNode ? "input-edit-node" : "py-2",
|
||||
)}
|
||||
>
|
||||
<span data-testid={`value-dropdown-` + id}>
|
||||
{value &&
|
||||
value !== "" &&
|
||||
options.find((option) => option === value)
|
||||
? options.find((option) => option === value)
|
||||
: "Choose an option..."}
|
||||
</span>
|
||||
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
className="ml-2 h-4 w-4 shrink-0 opacity-50"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContentWithoutPortal
|
||||
className="nocopy nowheel nopan nodelete nodrag noundo w-full p-0"
|
||||
style={{ minWidth: refButton?.current?.clientWidth ?? "200px" }}
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
className="ml-2 h-4 w-4 shrink-0 opacity-50"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<PopoverContentDropdown
|
||||
side="bottom"
|
||||
avoidCollisions={!!children}
|
||||
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
|
||||
style={
|
||||
children
|
||||
? {}
|
||||
: { minWidth: refButton?.current?.clientWidth ?? "200px" }
|
||||
}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search options..." className="h-9" />
|
||||
|
|
@ -90,7 +107,7 @@ export default function Dropdown({
|
|||
name="Check"
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0"
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
|
@ -98,7 +115,7 @@ export default function Dropdown({
|
|||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</PopoverContentDropdown>
|
||||
</Popover>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
|||
import { FlowType } from "../../types/flow";
|
||||
import { updateIds } from "../../utils/reactflowUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import ShadTooltip from "../shadTooltipComponent";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function FloatComponent({
|
|||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import { nodeIconsLucide } from "../../utils/styleUtils";
|
|||
import { cn } from "../../utils/utils";
|
||||
import Loading from "../ui/loading";
|
||||
|
||||
const ForwardedIconComponent = memo(
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ForwardedIconComponent = memo(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
|
|
@ -16,11 +18,20 @@ const ForwardedIconComponent = memo(
|
|||
strokeWidth,
|
||||
id = "",
|
||||
}: IconComponentProps,
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const [showFallback, setShowFallback] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowFallback(true);
|
||||
}, 30);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
let TargetIcon = nodeIconsLucide[name];
|
||||
if (!TargetIcon) {
|
||||
// check if name exists in dynamicIconImports
|
||||
if (!dynamicIconImports[name]) {
|
||||
TargetIcon = nodeIconsLucide["unknown"];
|
||||
} else TargetIcon = lazy(dynamicIconImports[name]);
|
||||
|
|
@ -35,11 +46,15 @@ const ForwardedIconComponent = memo(
|
|||
if (!TargetIcon) {
|
||||
return null; // Render nothing until the icon is loaded
|
||||
}
|
||||
const fallback = (
|
||||
|
||||
const fallback = showFallback ? (
|
||||
<div className={cn(className, "flex items-center justify-center")}>
|
||||
<Loading />
|
||||
</div>
|
||||
) : (
|
||||
<div className={className}></div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
<TargetIcon
|
||||
|
|
@ -50,8 +65,8 @@ const ForwardedIconComponent = memo(
|
|||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export default ForwardedIconComponent;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { gradients } from "../../utils/styleUtils";
|
|||
|
||||
export default function GradientChooserComponent({ value, onChange }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<div className="flex flex-wrap items-center justify-start gap-2">
|
||||
{gradients.map((gradient, idx) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ import { Node } from "reactflow";
|
|||
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
|
||||
import { SAVED_HOVER } from "../../../../constants/constants";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import FlowLogsModal from "../../../../modals/flowLogsModal";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import ShadTooltip from "../../../ShadTooltipComponent";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = ({
|
||||
|
|
@ -34,6 +35,7 @@ export const MenuBar = ({
|
|||
const redo = useFlowsManagerStore((state) => state.redo);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [openLogs, setOpenLogs] = useState(false);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -123,6 +125,18 @@ export const MenuBar = ({
|
|||
/>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenLogs(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="ScrollText"
|
||||
className="header-menu-options "
|
||||
/>
|
||||
Logs
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
|
@ -132,7 +146,7 @@ export const MenuBar = ({
|
|||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [error],
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
@ -194,6 +208,7 @@ export const MenuBar = ({
|
|||
open={openSettings}
|
||||
setOpen={setOpenSettings}
|
||||
></FlowSettingsModal>
|
||||
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
|
||||
</div>
|
||||
{(currentFlow.updated_at || saveLoading) && (
|
||||
<ShadTooltip
|
||||
|
|
@ -213,7 +228,7 @@ export const MenuBar = ({
|
|||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
|
||||
)}
|
||||
/>
|
||||
{printByBuildStatus()}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default function Header(): JSX.Element {
|
|||
<Button
|
||||
className="gap-2"
|
||||
variant={
|
||||
location.pathname === "/flows" ||
|
||||
location.pathname === "/all" ||
|
||||
location.pathname === "/components"
|
||||
? "primary"
|
||||
: "secondary"
|
||||
|
|
@ -73,18 +73,7 @@ export default function Header(): JSX.Element {
|
|||
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
|
||||
</Button>
|
||||
</Link>
|
||||
{/* <Link to="/community">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={
|
||||
location.pathname === "/community" ? "primary" : "secondary"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<IconComponent name="Users2" className="h-4 w-4" />
|
||||
<div className="flex-1">Community Examples</div>
|
||||
</Button>
|
||||
</Link> */}
|
||||
|
||||
{hasStore && (
|
||||
<Link to="/store">
|
||||
<Button
|
||||
|
|
@ -168,50 +157,57 @@ export default function Header(): JSX.Element {
|
|||
/>
|
||||
</button>
|
||||
)}
|
||||
{!autoLogin && (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={
|
||||
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
||||
(userData?.profile_image ??
|
||||
gradients[
|
||||
parseInt(userData?.id ?? "", 30) % gradients.length
|
||||
])
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{isAdmin && (
|
||||
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
data-testid="user-profile-settings"
|
||||
className={
|
||||
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
||||
(userData?.profile_image ??
|
||||
(userData?.id
|
||||
? gradients[
|
||||
parseInt(userData?.id ?? "", 30) % gradients.length
|
||||
]
|
||||
: "bg-gray-500"))
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>General</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
{!autoLogin && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/admin")}
|
||||
>
|
||||
Admin Page
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/admin")}
|
||||
onClick={() => {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
Admin Page
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/account/settings")}
|
||||
>
|
||||
Profile Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function HorizontalScrollFadeComponent({
|
||||
children,
|
||||
isFolder = true,
|
||||
}) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const fadeContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [divWidth, setDivWidth] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
setDivWidth(scrollContainerRef.current.clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize(); // call the function at start to get the initial width
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } =
|
||||
scrollContainerRef.current;
|
||||
const atStart = scrollLeft === 0;
|
||||
const atEnd = scrollLeft === scrollWidth - clientWidth;
|
||||
const isScrollable = scrollWidth > clientWidth;
|
||||
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-left",
|
||||
isScrollable && !atStart,
|
||||
);
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-right",
|
||||
isScrollable && !atEnd,
|
||||
);
|
||||
};
|
||||
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("scroll", handleScroll);
|
||||
// Delay the initial scroll event dispatch to ensure correct calculation
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
return () => scrollContainer.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [divWidth, children]); // Depend on divWidth
|
||||
|
||||
return isFolder ? (
|
||||
<div className="hidden w-full flex-col gap-2 lg:flex">{children}</div>
|
||||
) : (
|
||||
<div ref={fadeContainerRef} className="fade-container flex">
|
||||
<div ref={scrollContainerRef} className="scroll-container flex gap-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import { classNames, cn } from "../../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../../../ui/command";
|
||||
import { Input } from "../../../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
|
||||
const CustomInputPopover = ({
|
||||
id,
|
||||
refInput,
|
||||
onInputLostFocus,
|
||||
selectedOption,
|
||||
setSelectedOption,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
value,
|
||||
autoFocus,
|
||||
disabled,
|
||||
setShowOptions,
|
||||
required,
|
||||
className,
|
||||
password,
|
||||
pwdVisible,
|
||||
editNode,
|
||||
placeholder,
|
||||
onChange,
|
||||
blurOnEnter,
|
||||
options,
|
||||
optionsPlaceholder,
|
||||
optionButton,
|
||||
optionsButton,
|
||||
handleKeyDown,
|
||||
showOptions,
|
||||
}) => {
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
if (password) {
|
||||
if (
|
||||
e.target.value.split("").every((char) => char === "•") &&
|
||||
e.target.value !== ""
|
||||
) {
|
||||
setErrorData({
|
||||
title: `Invalid characters: ${e.target.value}`,
|
||||
list: [
|
||||
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
onChange && onChange(e.target.value);
|
||||
};
|
||||
return (
|
||||
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onInputLostFocus}
|
||||
value={
|
||||
(selectedOption !== "" || !onChange) && setSelectedOption
|
||||
? selectedOption
|
||||
: (selectedOptions?.length !== 0 || !onChange) &&
|
||||
setSelectedOptions
|
||||
? selectedOptions?.join(", ")
|
||||
: value
|
||||
}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
|
||||
((selectedOptions?.length !== 0 || !onChange) &&
|
||||
setSelectedOptions)) &&
|
||||
setShowOptions(true);
|
||||
}}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password &&
|
||||
(!setSelectedOption || selectedOption === "") &&
|
||||
!pwdVisible &&
|
||||
value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && (setSelectedOption || setSelectedOptions)
|
||||
? "pr-[62.9px]"
|
||||
: "",
|
||||
(!password && (setSelectedOption || setSelectedOptions)) ||
|
||||
(password && !(setSelectedOption || setSelectedOptions))
|
||||
? "pr-8"
|
||||
: "",
|
||||
className!,
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e);
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
className="nocopy nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (
|
||||
value.toLowerCase().includes(search.toLowerCase()) ||
|
||||
value.includes("doNotFilter-")
|
||||
)
|
||||
return 1;
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder={optionsPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
{options.map((option, id) => (
|
||||
<CommandItem
|
||||
className="group"
|
||||
key={option + id}
|
||||
value={option}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedOption &&
|
||||
setSelectedOption(
|
||||
currentValue === selectedOption ? "" : currentValue,
|
||||
);
|
||||
setSelectedOptions &&
|
||||
setSelectedOptions(
|
||||
selectedOptions?.includes(currentValue)
|
||||
? selectedOptions.filter(
|
||||
(item) => item !== currentValue,
|
||||
)
|
||||
: [...selectedOptions, currentValue],
|
||||
);
|
||||
!setSelectedOptions && setShowOptions(false);
|
||||
}}
|
||||
>
|
||||
<div className="group flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"relative mr-2 h-4 w-4",
|
||||
selectedOption === option ||
|
||||
selectedOptions?.includes(option)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className="mr-2 h-4 w-4 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
|
||||
<ForwardedIconComponent
|
||||
name="X"
|
||||
className="mr-2 h-4 w-4 text-status-red"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{option}
|
||||
</div>
|
||||
{optionButton && optionButton(option)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{optionsButton && optionsButton}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomInputPopover;
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import { classNames, cn } from "../../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../../../ui/command";
|
||||
import { Input } from "../../../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
|
||||
const CustomInputPopoverObject = ({
|
||||
id,
|
||||
refInput,
|
||||
onInputLostFocus,
|
||||
selectedOption,
|
||||
setSelectedOption,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
value,
|
||||
autoFocus,
|
||||
disabled,
|
||||
setShowOptions,
|
||||
required,
|
||||
className,
|
||||
placeholder,
|
||||
onChange,
|
||||
blurOnEnter,
|
||||
options,
|
||||
optionsPlaceholder,
|
||||
optionButton,
|
||||
optionsButton,
|
||||
handleKeyDown,
|
||||
showOptions,
|
||||
}) => {
|
||||
const handleInputChange = (e) => {
|
||||
onChange && onChange(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onInputLostFocus}
|
||||
value={
|
||||
(selectedOption !== "" || !onChange) && setSelectedOption
|
||||
? options.find((option) => option.id === selectedOption)?.name ||
|
||||
""
|
||||
: (selectedOptions?.length !== 0 || !onChange) &&
|
||||
setSelectedOptions
|
||||
? selectedOptions
|
||||
.map(
|
||||
(optionId) =>
|
||||
options.find((option) => option.id === optionId)?.name,
|
||||
)
|
||||
.join(", ")
|
||||
: value
|
||||
}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
|
||||
((selectedOptions?.length !== 0 || !onChange) &&
|
||||
setSelectedOptions)) &&
|
||||
setShowOptions(true);
|
||||
}}
|
||||
required={required}
|
||||
className={classNames(className!)}
|
||||
placeholder={placeholder}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e);
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
data-testid={id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
className="nocopy nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (
|
||||
value.toLowerCase().includes(search.toLowerCase()) ||
|
||||
value.includes("doNotFilter-")
|
||||
)
|
||||
return 1;
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder={optionsPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
{options.map((option, index) => (
|
||||
<CommandItem
|
||||
className="group"
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedOption &&
|
||||
setSelectedOption(
|
||||
currentValue === selectedOption ? "" : currentValue,
|
||||
);
|
||||
setSelectedOptions &&
|
||||
setSelectedOptions(
|
||||
selectedOptions?.includes(currentValue)
|
||||
? selectedOptions.filter(
|
||||
(item) => item !== currentValue,
|
||||
)
|
||||
: [...selectedOptions, currentValue],
|
||||
);
|
||||
!setSelectedOptions && setShowOptions(false);
|
||||
}}
|
||||
>
|
||||
<div className="group flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"relative mr-2 h-4 w-4",
|
||||
selectedOption === option.id ||
|
||||
selectedOptions?.includes(option.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className="mr-2 h-4 w-4 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
|
||||
<ForwardedIconComponent
|
||||
name="X"
|
||||
className="mr-2 h-4 w-4 text-status-red"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span data-testid={`option-${index}`}>
|
||||
{option.name}{" "}
|
||||
</span>
|
||||
|
||||
{/* Display the name property of the option */}
|
||||
</div>
|
||||
{optionButton && optionButton(option)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{optionsButton && optionsButton}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomInputPopoverObject;
|
||||
|
|
@ -1,20 +1,12 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../ui/command";
|
||||
import { Input } from "../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../ui/popover";
|
||||
import CustomInputPopover from "./components/popover";
|
||||
import CustomInputPopoverObject from "./components/popoverObject";
|
||||
|
||||
export default function InputComponent({
|
||||
autoFocus = false,
|
||||
|
|
@ -33,20 +25,22 @@ export default function InputComponent({
|
|||
optionsIcon = "ChevronsUpDown",
|
||||
selectedOption,
|
||||
setSelectedOption,
|
||||
selectedOptions = [],
|
||||
setSelectedOptions,
|
||||
options = [],
|
||||
optionsPlaceholder = "Search options...",
|
||||
optionsButton,
|
||||
optionButton,
|
||||
objectOptions,
|
||||
isObjectOption = false,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value && onChange && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
@ -74,7 +68,7 @@ export default function InputComponent({
|
|||
editNode ? " input-edit-node " : "",
|
||||
password && editNode ? "pr-8" : "",
|
||||
password && !editNode ? "pr-10" : "",
|
||||
className!
|
||||
className!,
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
|
|
@ -91,176 +85,82 @@ export default function InputComponent({
|
|||
</Form.Control>
|
||||
) : (
|
||||
<>
|
||||
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onInputLostFocus}
|
||||
value={
|
||||
(selectedOption !== "" || !onChange) && setSelectedOption
|
||||
? selectedOption
|
||||
: value
|
||||
}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
(selectedOption !== "" || !onChange) &&
|
||||
setSelectedOption &&
|
||||
setShowOptions(true);
|
||||
}}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password &&
|
||||
selectedOption === "" &&
|
||||
!pwdVisible &&
|
||||
value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && setSelectedOption ? "pr-[62.9px]" : "",
|
||||
(!password && setSelectedOption) ||
|
||||
(password && !setSelectedOption)
|
||||
? "pr-8"
|
||||
: "",
|
||||
|
||||
className!
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
// if the user copies a password from another input
|
||||
// it might come as ••••••••••• it causes errors
|
||||
// in ascii encoding, so we need to handle it
|
||||
if (password) {
|
||||
// check if all chars are •
|
||||
if (
|
||||
e.target.value.split("").every((char) => char === "•") &&
|
||||
e.target.value !== ""
|
||||
) {
|
||||
setErrorData({
|
||||
title: `Invalid characters: ${e.target.value}`,
|
||||
list: [
|
||||
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter")
|
||||
refInput.current?.blur();
|
||||
}}
|
||||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
className="nocopy nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.includes(search) || value.includes("doNotFilter-"))
|
||||
return 1; // ensures items arent filtered
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder={optionsPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
{options.map((option, id) => (
|
||||
<CommandItem
|
||||
className="group"
|
||||
key={option + id}
|
||||
value={option}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedOption &&
|
||||
setSelectedOption(
|
||||
currentValue === selectedOption
|
||||
? ""
|
||||
: currentValue
|
||||
);
|
||||
setShowOptions(false);
|
||||
}}
|
||||
>
|
||||
<div className="group flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"relative mr-2 h-4 w-4",
|
||||
selectedOption === option
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className="mr-2 h-4 w-4 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
|
||||
<ForwardedIconComponent
|
||||
name="X"
|
||||
className="mr-2 h-4 w-4 text-status-red"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{option}
|
||||
</div>
|
||||
{optionButton && optionButton(option)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{optionsButton && optionsButton}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</Popover>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute inset-y-0 h-full w-full cursor-pointer",
|
||||
(selectedOption !== "" || !onChange) && setSelectedOption
|
||||
? ""
|
||||
: "hidden"
|
||||
)}
|
||||
onClick={
|
||||
(selectedOption !== "" || !onChange) && setSelectedOption
|
||||
? (e) => {
|
||||
setShowOptions((old) => !old);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
: () => {}
|
||||
}
|
||||
></div>
|
||||
{isObjectOption ? (
|
||||
// Content to render when isObjectOption is true
|
||||
<CustomInputPopoverObject
|
||||
refInput={refInput}
|
||||
handleKeyDown={handleKeyDown}
|
||||
optionButton={optionButton}
|
||||
optionsButton={optionsButton}
|
||||
showOptions={showOptions}
|
||||
onChange={onChange}
|
||||
id={`object-${id}`}
|
||||
onInputLostFocus={onInputLostFocus}
|
||||
selectedOption={selectedOption}
|
||||
setSelectedOption={setSelectedOption}
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
options={objectOptions}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
setShowOptions={setShowOptions}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
blurOnEnter={blurOnEnter}
|
||||
optionsPlaceholder={optionsPlaceholder}
|
||||
className={className}
|
||||
/>
|
||||
) : (
|
||||
<CustomInputPopover
|
||||
refInput={refInput}
|
||||
handleKeyDown={handleKeyDown}
|
||||
optionButton={optionButton}
|
||||
optionsButton={optionsButton}
|
||||
showOptions={showOptions}
|
||||
onChange={onChange}
|
||||
id={`popover-anchor-${id}`}
|
||||
onInputLostFocus={onInputLostFocus}
|
||||
selectedOption={selectedOption}
|
||||
setSelectedOption={setSelectedOption}
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
setShowOptions={setShowOptions}
|
||||
required={required}
|
||||
password={password}
|
||||
pwdVisible={pwdVisible}
|
||||
editNode={editNode}
|
||||
placeholder={placeholder}
|
||||
blurOnEnter={blurOnEnter}
|
||||
options={options}
|
||||
optionsPlaceholder={optionsPlaceholder}
|
||||
className={className}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{setSelectedOption && (
|
||||
{(setSelectedOption || setSelectedOptions) && (
|
||||
<span
|
||||
className={cn(
|
||||
password && selectedOption === "" ? "right-8" : "right-0",
|
||||
"absolute inset-y-0 flex items-center pr-2.5"
|
||||
"absolute inset-y-0 flex items-center pr-2.5",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
setShowOptions(!showOptions);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={cn(
|
||||
selectedOption !== ""
|
||||
? "text-medium-indigo"
|
||||
: "text-muted-foreground",
|
||||
"hover:text-accent-foreground"
|
||||
"hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
|
|
@ -272,7 +172,7 @@ export default function InputComponent({
|
|||
</span>
|
||||
)}
|
||||
|
||||
{password && selectedOption === "" && (
|
||||
{password && (!setSelectedOption || selectedOption === "") && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
|
|
@ -280,59 +180,57 @@ export default function InputComponent({
|
|||
"mb-px",
|
||||
editNode
|
||||
? "input-component-true-button"
|
||||
: "input-component-false-button"
|
||||
: "input-component-false-button",
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setPwdVisible(!pwdVisible);
|
||||
}}
|
||||
>
|
||||
{password &&
|
||||
selectedOption === "" &&
|
||||
(pwdVisible ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "input-component-true-svg"
|
||||
: "input-component-false-svg"
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "input-component-true-svg"
|
||||
: "input-component-false-svg"
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
))}
|
||||
{pwdVisible ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "input-component-true-svg"
|
||||
: "input-component-false-svg",
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "input-component-true-svg"
|
||||
: "input-component-false-svg",
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default function InputFileComponent({
|
|||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
onFileChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
|
@ -106,8 +106,8 @@ export default function InputFileComponent({
|
|||
editNode
|
||||
? "input-edit-node input-dialog text-muted-foreground"
|
||||
: disabled
|
||||
? "input-disable input-dialog primary-input"
|
||||
: "input-dialog primary-input text-muted-foreground"
|
||||
? "input-disable input-dialog primary-input"
|
||||
: "input-dialog primary-input text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "No file"}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { useEffect } from "react";
|
||||
import { deleteGlobalVariable } from "../../controllers/API";
|
||||
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
|
||||
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariables";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
|
||||
import { InputGlobalComponentType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import AddNewVariableButton from "../addNewVariableButtonComponent/addNewVariableButton";
|
||||
|
|
@ -20,31 +19,54 @@ export default function InputGlobalComponent({
|
|||
editNode = false,
|
||||
}: InputGlobalComponentType): JSX.Element {
|
||||
const globalVariablesEntries = useGlobalVariablesStore(
|
||||
(state) => state.globalVariablesEntries
|
||||
(state) => state.globalVariablesEntries,
|
||||
);
|
||||
|
||||
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
|
||||
const unavaliableFields = useGlobalVariablesStore(
|
||||
(state) => state.unavaliableFields,
|
||||
);
|
||||
const removeGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.removeGlobalVariable
|
||||
(state) => state.removeGlobalVariable,
|
||||
);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.node?.template[name])
|
||||
if (
|
||||
globalVariablesEntries &&
|
||||
!globalVariablesEntries.includes(data.node?.template[name].value) &&
|
||||
data.node?.template[name].load_from_db
|
||||
) {
|
||||
onChange("");
|
||||
setDb(false);
|
||||
setTimeout(() => {
|
||||
onChange("", true);
|
||||
setDb(false);
|
||||
}, 100);
|
||||
}
|
||||
}, [globalVariablesEntries]);
|
||||
|
||||
function handleDelete(key: string) {
|
||||
useEffect(() => {
|
||||
if (
|
||||
!data.node?.template[name].value &&
|
||||
data.node?.template[name].display_name
|
||||
) {
|
||||
if (
|
||||
unavaliableFields[data.node?.template[name].display_name!] &&
|
||||
!disabled
|
||||
) {
|
||||
setTimeout(() => {
|
||||
setDb(true);
|
||||
onChange(unavaliableFields[data.node?.template[name].display_name!]);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [unavaliableFields]);
|
||||
|
||||
async function handleDelete(key: string) {
|
||||
const id = getVariableId(key);
|
||||
if (id !== undefined) {
|
||||
deleteGlobalVariable(id)
|
||||
.then((_) => {
|
||||
await deleteGlobalVariable(id)
|
||||
.then(() => {
|
||||
removeGlobalVariable(key);
|
||||
if (
|
||||
data?.node?.template[name].value === key &&
|
||||
|
|
@ -54,11 +76,10 @@ export default function InputGlobalComponent({
|
|||
setDb(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
.catch(() => {
|
||||
setErrorData({
|
||||
title: "Error deleting variable",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
list: [cn("ID not found for variable: ", key)],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
|
@ -109,7 +130,7 @@ export default function InputGlobalComponent({
|
|||
<ForwardedIconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100"
|
||||
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -117,7 +138,9 @@ export default function InputGlobalComponent({
|
|||
</DeleteConfirmationModal>
|
||||
)}
|
||||
selectedOption={
|
||||
data?.node?.template[name].load_from_db ?? false
|
||||
data?.node?.template[name].load_from_db &&
|
||||
globalVariablesEntries &&
|
||||
globalVariablesEntries.includes(data?.node?.template[name].value ?? "")
|
||||
? data?.node?.template[name].value
|
||||
: ""
|
||||
}
|
||||
|
|
@ -125,8 +148,8 @@ export default function InputGlobalComponent({
|
|||
onChange(value);
|
||||
setDb(value !== "" ? true : false);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
onChange={(value, skipSnapshot) => {
|
||||
onChange(value, skipSnapshot);
|
||||
setDb(false);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default function InputListComponent({
|
|||
disabled,
|
||||
editNode = false,
|
||||
componentName,
|
||||
playgroundDisabled,
|
||||
}: InputListComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled && value.length > 0 && value[0] !== "") {
|
||||
|
|
@ -24,7 +25,7 @@ export default function InputListComponent({
|
|||
value = [value];
|
||||
}
|
||||
|
||||
if (!value.length) value = [""];
|
||||
if (!value?.length) value = [""];
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -37,7 +38,7 @@ export default function InputListComponent({
|
|||
return (
|
||||
<div key={idx} className="flex w-full gap-3">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
disabled={disabled || playgroundDisabled}
|
||||
type="text"
|
||||
value={singleValue}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
|
|
@ -64,6 +65,7 @@ export default function InputListComponent({
|
|||
editNode ? "-edit" : ""
|
||||
}_${componentName}-` + idx
|
||||
}
|
||||
disabled={disabled || playgroundDisabled}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
|
|
@ -82,10 +84,15 @@ export default function InputListComponent({
|
|||
newInputList.splice(idx, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
disabled={disabled || playgroundDisabled}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
className="h-4 w-4 hover:text-status-red"
|
||||
className={`h-4 w-4 ${
|
||||
disabled || playgroundDisabled
|
||||
? ""
|
||||
: "hover:text-accent-foreground"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default function IntComponent({
|
|||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ export default function KeypairListComponent({
|
|||
}
|
||||
}, [disabled]);
|
||||
|
||||
const ref = useRef(value.length === 0 ? [{ "": "" }] : value);
|
||||
const checkValueType = (value) => {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
};
|
||||
|
||||
const ref = useRef<any>([]);
|
||||
ref.current =
|
||||
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(value) !== JSON.stringify(ref.current)) {
|
||||
|
|
|
|||
7
src/frontend/src/components/numberReader/index.tsx
Normal file
7
src/frontend/src/components/numberReader/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function NumberReader({
|
||||
number,
|
||||
}: {
|
||||
number: number;
|
||||
}): JSX.Element {
|
||||
return <span>{number}</span>;
|
||||
}
|
||||
13
src/frontend/src/components/objectRender/index.tsx
Normal file
13
src/frontend/src/components/objectRender/index.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import DictAreaModal from "../../modals/dictAreaModal";
|
||||
|
||||
export default function ObjectRender({ object }: { object: any }): JSX.Element {
|
||||
//TODO check object type
|
||||
|
||||
return (
|
||||
<DictAreaModal value={object}>
|
||||
<div className="flex h-full w-full items-center align-middle transition-all hover:scale-105">
|
||||
<div className="truncate">{JSON.stringify(object)}</div>
|
||||
</div>
|
||||
</DictAreaModal>
|
||||
);
|
||||
}
|
||||
19
src/frontend/src/components/pdfViewer/Error/index.tsx
Normal file
19
src/frontend/src/components/pdfViewer/Error/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
||||
export default function Error(): JSX.Element {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="chat-alert-box">
|
||||
<span className="flex gap-2">
|
||||
<IconComponent name="FileX2" />
|
||||
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="langflow-chat-desc">
|
||||
<span className="langflow-chat-desc-span">{PDFCheckFlow} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
155
src/frontend/src/components/pdfViewer/index.tsx
Normal file
155
src/frontend/src/components/pdfViewer/index.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
||||
import "react-pdf/dist/esm/Page/TextLayer.css";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import Loading from "../ui/loading";
|
||||
import Error from "./Error";
|
||||
import NoDataPdf from "./noData";
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
|
||||
export default function PdfViewer({ pdf }: { pdf: string }): JSX.Element {
|
||||
const [numPages, setNumPages] = useState(-1);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [width, setWidth] = useState<number | undefined>(undefined);
|
||||
const [showControl, setShowControl] = useState(false);
|
||||
const container = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
//shortcuts to change page
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "ArrowLeft") {
|
||||
if (pageNumber > 1) previousPage();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
if (pageNumber < numPages) nextPage();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [pageNumber]);
|
||||
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setNumPages(numPages);
|
||||
setPageNumber(1);
|
||||
}
|
||||
|
||||
function changePage(offset) {
|
||||
setPageNumber((prevPageNumber) => prevPageNumber + offset);
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
changePage(-1);
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
changePage(1);
|
||||
}
|
||||
|
||||
//set handle scale in % to real number
|
||||
function handleScaleChange(e) {
|
||||
//check if e is a number
|
||||
if (isNaN(e) || e < 0.1) return;
|
||||
// round to 2 decimal places
|
||||
e = Math.round(e * 10) / 10;
|
||||
|
||||
setScale(e);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
handleScaleChange(scale + 0.1);
|
||||
}
|
||||
function zoomOut() {
|
||||
if (scale > 0.1) handleScaleChange(scale - 0.1);
|
||||
}
|
||||
|
||||
function handlePageLoad(page) {
|
||||
if (!container.current) return;
|
||||
const containerWidth = container.current.clientWidth;
|
||||
const pageWidth = page.width;
|
||||
if (containerWidth > pageWidth) {
|
||||
setWidth(containerWidth - 10);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
onMouseEnter={(_) => setShowControl(true)}
|
||||
onMouseLeave={(_) => setShowControl(false)}
|
||||
className="flex h-full w-full flex-col items-center justify-end overflow-clip rounded-lg border border-border"
|
||||
>
|
||||
<div className={"h-full min-h-0 w-full overflow-auto custom-scroll"}>
|
||||
<Document
|
||||
loading={
|
||||
<div className="flex h-full w-full items-center justify-center align-middle">
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
file={pdf}
|
||||
noData={<NoDataPdf />}
|
||||
error={<Error />}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<Page
|
||||
width={width}
|
||||
onLoadSuccess={handlePageLoad}
|
||||
scale={scale}
|
||||
renderTextLayer
|
||||
pageNumber={pageNumber}
|
||||
className={"h-full max-h-0 w-full"}
|
||||
/>
|
||||
</Document>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"absolute z-50 pb-5 " + (showControl && numPages > 0 ? "" : " hidden")
|
||||
}
|
||||
>
|
||||
<div className=" flex w-min items-center justify-center gap-0.5 rounded-xl bg-secondary px-2 align-middle">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pageNumber <= 1}
|
||||
onClick={previousPage}
|
||||
>
|
||||
<IconComponent
|
||||
name={"ChevronLeft"}
|
||||
className="h-6 w-6"
|
||||
></IconComponent>
|
||||
</button>
|
||||
<p>
|
||||
{pageNumber || (numPages ? 1 : "--")}/{numPages || "--"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pageNumber >= numPages}
|
||||
onClick={nextPage}
|
||||
>
|
||||
<IconComponent
|
||||
name={"ChevronRight"}
|
||||
className="h-6 w-6"
|
||||
></IconComponent>
|
||||
</button>
|
||||
<p className="px-2">|</p>
|
||||
<button type="button" onClick={zoomOut}>
|
||||
<IconComponent name={"ZoomOut"} className="h-6 w-6"></IconComponent>
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
step={0.1}
|
||||
className="w-6 border-b bg-transparent text-center arrow-hide"
|
||||
onChange={(e) => handleScaleChange(e.target.value)}
|
||||
value={scale}
|
||||
/>
|
||||
<button type="button" onClick={zoomIn}>
|
||||
<IconComponent name={"ZoomIn"} className="h-6 w-6"></IconComponent>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/frontend/src/components/pdfViewer/noData/index.tsx
Normal file
17
src/frontend/src/components/pdfViewer/noData/index.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { PDFErrorTitle, PDFLoadError } from "../../../constants/constants";
|
||||
|
||||
export default function NoDataPdf(): JSX.Element {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="chat-alert-box">
|
||||
<span>
|
||||
📄 <span className="langflow-chat-span">{PDFErrorTitle}</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="langflow-chat-desc">
|
||||
<span className="langflow-chat-desc-span">{PDFLoadError} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export default function PromptAreaComponent({
|
|||
}: PromptAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
34
src/frontend/src/components/recordsOutputComponent/index.tsx
Normal file
34
src/frontend/src/components/recordsOutputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
|
||||
import { FlowPoolObjectType } from "../../types/chat";
|
||||
import TableComponent from "../tableComponent";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
|
||||
function RecordsOutputComponent({
|
||||
flowPool,
|
||||
pagination,
|
||||
}: {
|
||||
flowPool: FlowPoolObjectType;
|
||||
pagination: boolean;
|
||||
}) {
|
||||
const rows = flowPool?.data?.artifacts?.records ?? [];
|
||||
const columns = extractColumnsFromRows(rows, "union");
|
||||
const columnDefs = columns.map((col, idx) => ({
|
||||
...col,
|
||||
resizable: idx !== columns.length - 1,
|
||||
flex: idx !== columns.length - 1 ? 1 : 2,
|
||||
})) as (ColDef<any> | ColGroupDef<any>)[];
|
||||
|
||||
return (
|
||||
<TableComponent
|
||||
overlayNoRowsTemplate="No data available"
|
||||
suppressRowClickSelection={true}
|
||||
pagination={pagination}
|
||||
columnDefs={columnDefs}
|
||||
rowData={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordsOutputComponent;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import { buttonVariants } from "../../../ui/button";
|
||||
|
||||
type SideBarButtonsComponentProps = {
|
||||
items: {
|
||||
href?: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}[];
|
||||
pathname: string;
|
||||
handleOpenNewFolderModal?: () => void;
|
||||
};
|
||||
const SideBarButtonsComponent = ({
|
||||
items,
|
||||
handleOpenNewFolderModal,
|
||||
}: SideBarButtonsComponentProps) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<Link to={item.href!}>
|
||||
<div
|
||||
key={item.title}
|
||||
data-testid={`sidebar-nav-${item.title}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent",
|
||||
)}
|
||||
onClick={handleOpenNewFolderModal}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SideBarButtonsComponent;
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import { useLocation } from "react-router-dom";
|
||||
import { FolderType } from "../../../../pages/MainPage/entities";
|
||||
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import DropdownButton from "../../../dropdownButtonComponent";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../genericIconComponent";
|
||||
import { Button, buttonVariants } from "../../../ui/button";
|
||||
import useFileDrop from "../../hooks/use-on-file-drop";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
|
||||
type SideBarFoldersButtonsComponentProps = {
|
||||
folders: FolderType[];
|
||||
pathname: string;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
handleAddFolder?: () => void;
|
||||
};
|
||||
const SideBarFoldersButtonsComponent = ({
|
||||
folders,
|
||||
pathname,
|
||||
handleAddFolder,
|
||||
handleChangeFolder,
|
||||
handleEditFolder,
|
||||
handleDeleteFolder,
|
||||
}: SideBarFoldersButtonsComponentProps) => {
|
||||
const uploadFolder = useFolderStore((state) => state.uploadFolder);
|
||||
const currentFolder = pathname.split("/");
|
||||
const urlWithoutPath = pathname.split("/").length < 4;
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
const allFlows = useFlowsManagerStore((state) => state.allFlows);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const checkPathName = (itemId: string) => {
|
||||
if (urlWithoutPath && itemId === myCollectionId) {
|
||||
return true;
|
||||
}
|
||||
return currentFolder.includes(itemId);
|
||||
};
|
||||
const location = useLocation();
|
||||
const folderId = location?.state?.folderId ?? myCollectionId;
|
||||
const getFolderById = useFolderStore((state) => state.getFolderById);
|
||||
|
||||
const handleFolderChange = (folderId: string) => {
|
||||
getFolderById(folderId);
|
||||
};
|
||||
|
||||
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
|
||||
folderId,
|
||||
handleFolderChange,
|
||||
);
|
||||
|
||||
const handleUploadFlowsToFolder = () => {
|
||||
uploadFolder(folderId);
|
||||
};
|
||||
|
||||
const handleDownloadFolder = (id: string) => {
|
||||
handleDownloadFolderFn(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex shrink-0 items-center justify-between">
|
||||
<DropdownButton
|
||||
firstButtonName="New Folder"
|
||||
onFirstBtnClick={handleAddFolder!}
|
||||
options={[]}
|
||||
plusButton={true}
|
||||
dropdownOptions={false}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleUploadFlowsToFolder}
|
||||
className="px-7"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Upload"
|
||||
className="main-page-nav-button"
|
||||
/>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
|
||||
<>
|
||||
{folders.map((item, index) => (
|
||||
<div
|
||||
onDragOver={dragOver}
|
||||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
onDrop={(e) => onDrop(e, item.id!)}
|
||||
key={item.id}
|
||||
data-testid={`sidebar-nav-${item.name}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
checkPathName(item.id!)
|
||||
? "border border-border bg-muted hover:bg-muted"
|
||||
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
|
||||
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
|
||||
)}
|
||||
onClick={() => handleChangeFolder!(item.id!)}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<IconComponent
|
||||
name={"folder"}
|
||||
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
|
||||
/>
|
||||
<span className="block max-w-full truncate opacity-100">
|
||||
{item.name}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
{index > 0 && (
|
||||
<Button
|
||||
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
|
||||
onClick={(e) => {
|
||||
handleDeleteFolder!(item);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<IconComponent
|
||||
name={"trash"}
|
||||
className=" w-4 stroke-[1.5]"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{index > 0 && (
|
||||
<Button
|
||||
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
|
||||
onClick={(e) => {
|
||||
handleEditFolder!(item);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<IconComponent
|
||||
name={"pencil"}
|
||||
className=" w-4 stroke-[1.5] text-white "
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
|
||||
onClick={(e) => {
|
||||
handleDownloadFolder(item.id!);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
variant={"ghost"}
|
||||
>
|
||||
<IconComponent
|
||||
name={"Download"}
|
||||
className=" w-4 stroke-[1.5] text-white "
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SideBarFoldersButtonsComponent;
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
UPLOAD_ALERT_LIST,
|
||||
WRONG_FILE_ERROR_ALERT,
|
||||
} from "../../../constants/alerts_constants";
|
||||
import { updateFlowInDatabase } from "../../../controllers/API";
|
||||
import { uploadFlowsFromFolders } from "../../../pages/MainPage/services";
|
||||
import useAlertStore from "../../../stores/alertStore";
|
||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||
import { useFolderStore } from "../../../stores/foldersStore";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
const useFileDrop = (folderId, folderChangeCallback) => {
|
||||
const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
|
||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
|
||||
const triggerFolderChange = (folderId) => {
|
||||
if (folderChangeCallback) {
|
||||
folderChangeCallback(folderId);
|
||||
}
|
||||
};
|
||||
const handleFileDrop = async (e) => {
|
||||
if (e.dataTransfer.types.some((type) => type === "Files")) {
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const firstFile = e.dataTransfer.files[0];
|
||||
if (firstFile.type === "application/json") {
|
||||
uploadFormData(firstFile);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: WRONG_FILE_ERROR_ALERT,
|
||||
list: [UPLOAD_ALERT_LIST],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dragOver = (
|
||||
e:
|
||||
| React.DragEvent<HTMLDivElement>
|
||||
| React.DragEvent<HTMLButtonElement>
|
||||
| React.DragEvent<HTMLAnchorElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||
setFolderDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const dragEnter = (
|
||||
e:
|
||||
| React.DragEvent<HTMLDivElement>
|
||||
| React.DragEvent<HTMLButtonElement>
|
||||
| React.DragEvent<HTMLAnchorElement>,
|
||||
) => {
|
||||
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||
setFolderDragging(true);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const dragLeave = (
|
||||
e:
|
||||
| React.DragEvent<HTMLDivElement>
|
||||
| React.DragEvent<HTMLButtonElement>
|
||||
| React.DragEvent<HTMLAnchorElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
if (e.target === e.currentTarget) {
|
||||
setFolderDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (
|
||||
e:
|
||||
| React.DragEvent<HTMLDivElement>
|
||||
| React.DragEvent<HTMLButtonElement>
|
||||
| React.DragEvent<HTMLAnchorElement>,
|
||||
folderId: string,
|
||||
) => {
|
||||
if (e?.dataTransfer?.getData("flow")) {
|
||||
const data = JSON.parse(e?.dataTransfer?.getData("flow"));
|
||||
|
||||
if (data) {
|
||||
uploadFromDragCard(data.id, folderId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
handleFileDrop(e);
|
||||
setFolderDragging(false);
|
||||
};
|
||||
|
||||
const uploadFromDragCard = (flowId, folderId) => {
|
||||
const selectedFlow = flows.find((flow) => flow.id === flowId);
|
||||
|
||||
if (!selectedFlow) {
|
||||
throw new Error("Flow not found");
|
||||
}
|
||||
|
||||
const updatedFlow: FlowType = {
|
||||
...selectedFlow,
|
||||
folder_id: folderId,
|
||||
};
|
||||
updateFlowInDatabase(updatedFlow).then(() => {
|
||||
getFoldersApi(true);
|
||||
triggerFolderChange(folderId);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFormData = (data) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", data);
|
||||
|
||||
uploadFlowsFromFolders(formData).then(() => {
|
||||
getFoldersApi(true);
|
||||
triggerFolderChange(folderId);
|
||||
refreshFlows();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
dragOver,
|
||||
dragEnter,
|
||||
dragLeave,
|
||||
onDrop,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFileDrop;
|
||||
|
|
@ -1,48 +1,61 @@
|
|||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { FolderType } from "../../pages/MainPage/entities";
|
||||
import { useFolderStore } from "../../stores/foldersStore";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { buttonVariants } from "../ui/button";
|
||||
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
|
||||
import SideBarButtonsComponent from "./components/sideBarButtons";
|
||||
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
type SidebarNavProps = {
|
||||
items: {
|
||||
href: string;
|
||||
href?: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}[];
|
||||
}
|
||||
handleOpenNewFolderModal?: () => void;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function SidebarNav({
|
||||
className,
|
||||
items,
|
||||
handleOpenNewFolderModal,
|
||||
handleChangeFolder,
|
||||
handleEditFolder,
|
||||
handleDeleteFolder,
|
||||
...props
|
||||
}: SidebarNavProps) {
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const loadingFolders = useFolderStore((state) => state.loading);
|
||||
const folders = useFolderStore((state) => state.folders);
|
||||
|
||||
const pathValues = ["folder", "components", "flows", "all"];
|
||||
const isFolderPath = pathValues.some((value) => pathname.includes(value));
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
data-testid={`sidebar-nav-${item.title}`}
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
pathname === item.href
|
||||
? "border border-border bg-muted hover:bg-muted"
|
||||
: "border border-transparent hover:border-border hover:bg-transparent",
|
||||
"justify-start gap-2"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
<nav className={cn(className)} {...props}>
|
||||
<HorizontalScrollFadeComponent>
|
||||
<SideBarButtonsComponent
|
||||
items={items}
|
||||
pathname={pathname}
|
||||
handleOpenNewFolderModal={handleOpenNewFolderModal}
|
||||
/>
|
||||
|
||||
{!loadingFolders && folders?.length > 0 && isFolderPath && (
|
||||
<SideBarFoldersButtonsComponent
|
||||
folders={folders}
|
||||
pathname={pathname}
|
||||
handleChangeFolder={handleChangeFolder}
|
||||
handleEditFolder={handleEditFolder}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
handleAddFolder={handleOpenNewFolderModal}
|
||||
/>
|
||||
)}
|
||||
</HorizontalScrollFadeComponent>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export const StoreGuard = ({ children }) => {
|
|||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
|
||||
if (!hasStore) {
|
||||
return <Navigate to="/flows" replace />;
|
||||
return <Navigate to="/all" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export default function StringReader({
|
||||
string,
|
||||
}: {
|
||||
string: string;
|
||||
}): JSX.Element {
|
||||
return <span className="truncate">{string}</span>;
|
||||
}
|
||||
60
src/frontend/src/components/tableAutoCellRender/index.tsx
Normal file
60
src/frontend/src/components/tableAutoCellRender/index.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cn, isTimeStampString } from "../../utils/utils";
|
||||
import ArrayReader from "../arrayReaderComponent";
|
||||
import DateReader from "../dateReaderComponent";
|
||||
import NumberReader from "../numberReader";
|
||||
import ObjectRender from "../objectRender";
|
||||
import StringReader from "../stringReaderComponent";
|
||||
import { Label } from "../ui/label";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
export default function TableAutoCellRender({
|
||||
value,
|
||||
}: CustomCellRendererProps) {
|
||||
function getCellType() {
|
||||
switch (typeof value) {
|
||||
case "object":
|
||||
if (value === null) {
|
||||
return String(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
return <ArrayReader array={value} />;
|
||||
} else if (value.definitions) {
|
||||
// use a custom render defined by the sender
|
||||
} else {
|
||||
return <ObjectRender object={value} />;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
if (isTimeStampString(value)) {
|
||||
return <DateReader date={value} />;
|
||||
}
|
||||
//TODO: REFACTOR FOR ANY LABEL NOT HARDCODED
|
||||
else if (value === "success") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
size="sq"
|
||||
className={cn(
|
||||
"min-w-min bg-success-background text-success-foreground hover:bg-success-background",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return <StringReader string={value} />;
|
||||
}
|
||||
break;
|
||||
case "number":
|
||||
return <NumberReader number={value} />;
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group flex h-full w-full items-center align-middle">
|
||||
{getCellType()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
src/frontend/src/components/tableComponent/index.tsx
Normal file
119
src/frontend/src/components/tableComponent/index.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
|
||||
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
||||
import { ElementRef, forwardRef, useCallback } from "react";
|
||||
import {
|
||||
DEFAULT_TABLE_ALERT_MSG,
|
||||
DEFAULT_TABLE_ALERT_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
|
||||
import { cn } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
|
||||
interface TableComponentProps extends AgGridReactProps {
|
||||
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
|
||||
rowData: NonNullable<AgGridReactProps["rowData"]>;
|
||||
alertTitle?: string;
|
||||
alertDescription?: string;
|
||||
}
|
||||
|
||||
const TableComponent = forwardRef<
|
||||
ElementRef<typeof AgGridReact>,
|
||||
TableComponentProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
alertTitle = DEFAULT_TABLE_ALERT_TITLE,
|
||||
alertDescription = DEFAULT_TABLE_ALERT_MSG,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
var currentRowHeight: number;
|
||||
var minRowHeight = 25;
|
||||
|
||||
const getRowHeight = useCallback(() => {
|
||||
return currentRowHeight;
|
||||
}, []);
|
||||
|
||||
const onGridReady = useCallback((params: any) => {
|
||||
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
|
||||
currentRowHeight = minRowHeight;
|
||||
}, []);
|
||||
|
||||
const updateRowHeight = (params: { api: any }) => {
|
||||
const bodyViewport = document.querySelector(".ag-body-viewport");
|
||||
if (!bodyViewport) {
|
||||
return;
|
||||
}
|
||||
var gridHeight = bodyViewport.clientHeight;
|
||||
var renderedRowCount = params.api.getDisplayedRowCount();
|
||||
|
||||
if (renderedRowCount * minRowHeight >= gridHeight) {
|
||||
if (currentRowHeight !== minRowHeight) {
|
||||
currentRowHeight = minRowHeight;
|
||||
params.api.resetRowHeights();
|
||||
}
|
||||
} else {
|
||||
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
|
||||
params.api.resetRowHeights();
|
||||
}
|
||||
};
|
||||
|
||||
const onFirstDataRendered = useCallback(
|
||||
(params: any) => {
|
||||
updateRowHeight(params);
|
||||
},
|
||||
[updateRowHeight],
|
||||
);
|
||||
|
||||
const onGridSizeChanged = useCallback(
|
||||
(params: any) => {
|
||||
updateRowHeight(params);
|
||||
},
|
||||
[updateRowHeight],
|
||||
);
|
||||
|
||||
if (props.rowData.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-md border">
|
||||
<Alert variant={"default"} className="w-fit">
|
||||
<ForwardedIconComponent
|
||||
name="AlertCircle"
|
||||
className="h-5 w-5 text-primary"
|
||||
/>
|
||||
<AlertTitle>{alertTitle}</AlertTitle>
|
||||
<AlertDescription>{alertDescription}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
|
||||
"ag-theme-shadcn flex h-full flex-col",
|
||||
)} // applying the grid theme
|
||||
>
|
||||
<AgGridReact
|
||||
{...props}
|
||||
className={cn(props.className, "custom-scroll")}
|
||||
getRowHeight={getRowHeight}
|
||||
onGridReady={onGridReady}
|
||||
onFirstDataRendered={onFirstDataRendered}
|
||||
onGridSizeChanged={onGridSizeChanged}
|
||||
defaultColDef={{
|
||||
minWidth: 100,
|
||||
}}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default TableComponent;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "../../utils/utils";
|
||||
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
export function TagsSelector({
|
||||
|
|
@ -24,87 +24,37 @@ export function TagsSelector({
|
|||
setSelectedTags(newArray);
|
||||
};
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const fadeContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [divWidth, setDivWidth] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
setDivWidth(scrollContainerRef.current.clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize(); // call the function at start to get the initial width
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } =
|
||||
scrollContainerRef.current;
|
||||
const atStart = scrollLeft === 0;
|
||||
const atEnd = scrollLeft === scrollWidth - clientWidth;
|
||||
const isScrollable = scrollWidth > clientWidth;
|
||||
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-left",
|
||||
isScrollable && !atStart
|
||||
);
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-right",
|
||||
isScrollable && !atEnd
|
||||
);
|
||||
};
|
||||
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("scroll", handleScroll);
|
||||
// Delay the initial scroll event dispatch to ensure correct calculation
|
||||
scrollContainer.dispatchEvent(new Event("scroll"));
|
||||
return () => scrollContainer.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [divWidth, loadingTags]); // Depend on divWidth
|
||||
|
||||
return (
|
||||
<div ref={fadeContainerRef} className="fade-container">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="scroll-container flex min-w-min gap-2"
|
||||
>
|
||||
{!loadingTags &&
|
||||
tags.map((tag, idx) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className={
|
||||
disabled
|
||||
? "cursor-not-allowed"
|
||||
: " overflow-hidden whitespace-nowrap"
|
||||
}
|
||||
onClick={() => {
|
||||
updateTags(tag.name);
|
||||
}}
|
||||
<HorizontalScrollFadeComponent isFolder={false}>
|
||||
{!loadingTags &&
|
||||
tags.map((tag, idx) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className={
|
||||
disabled
|
||||
? "cursor-not-allowed"
|
||||
: " overflow-hidden whitespace-nowrap"
|
||||
}
|
||||
onClick={() => {
|
||||
updateTags(tag.name);
|
||||
}}
|
||||
key={idx}
|
||||
data-testid={`tag-selector-${tag.name}`}
|
||||
>
|
||||
<Badge
|
||||
key={idx}
|
||||
data-testid={`tag-selector-${tag.name}`}
|
||||
variant="outline"
|
||||
size="sq"
|
||||
className={cn(
|
||||
selectedTags.some((category) => category === tag.name)
|
||||
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Badge
|
||||
key={idx}
|
||||
variant="outline"
|
||||
size="sq"
|
||||
className={cn(
|
||||
selectedTags.some((category) => category === tag.name)
|
||||
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</HorizontalScrollFadeComponent>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function TextAreaComponent({
|
|||
// Clear text area
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
58
src/frontend/src/components/ui/alert.tsx
Normal file
58
src/frontend/src/components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
|
@ -13,14 +13,14 @@ const Checkbox = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
<IconComponent name="Check" className="h-4 w-4 stroke-2" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
|
@ -37,7 +37,7 @@ const CheckBoxDiv = ({
|
|||
className={cn(
|
||||
className,
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary text-primary-foreground" : ""
|
||||
checked ? "bg-primary text-primary-foreground" : "",
|
||||
)}
|
||||
>
|
||||
{checked && (
|
||||
|
|
|
|||
55
src/frontend/src/components/ui/custom-accordion.tsx
Normal file
55
src/frontend/src/components/ui/custom-accordion.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"use client";
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn("", className)} {...props} />
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger asChild ref={ref} {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
" flex flex-1 cursor-pointer items-center justify-between border-[1px] py-2 text-sm font-medium data-[state=closed]:rounded-md data-[state=open]:rounded-t-md data-[state=open]:border-b-0 data-[state=open]:bg-muted [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="h-4 w-4 font-bold text-primary transition-transform duration-200" />
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden border-[1px] text-sm data-[state=open]:rounded-b-md data-[state=open]:border-t-0 data-[state=open]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="pt-0">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
176
src/frontend/src/components/ui/form.tsx
Normal file
176
src/frontend/src/components/ui/form.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { Label } from "./label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
|
|
@ -95,6 +95,12 @@ export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
|
|||
*/
|
||||
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Flow Logs (Menubar)
|
||||
* @constant
|
||||
*/
|
||||
export const LOGS_DIALOG_SUBTITLE = "Check out information about your flow.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Code Dialog (Toolbar)
|
||||
* @constant
|
||||
|
|
@ -125,7 +131,6 @@ export const CODE_PROMPT_DIALOG_SUBTITLE =
|
|||
|
||||
export const CODE_DICT_DIALOG_SUBTITLE =
|
||||
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Prompt Dialog
|
||||
* @constant
|
||||
|
|
@ -161,6 +166,29 @@ export const IMPORT_DIALOG_SUBTITLE =
|
|||
*/
|
||||
export const TOOLTIP_EMPTY = "No compatible components found.";
|
||||
|
||||
export const CSVViewErrorTitle = "CSV output";
|
||||
|
||||
export const CSVNoDataError = "No data available";
|
||||
|
||||
export const PDFViewConstant = "Expand the ouptut to see the PDF";
|
||||
|
||||
export const CSVError = "Error loading CSV";
|
||||
|
||||
export const PDFLoadErrorTitle = "Error loading PDF";
|
||||
|
||||
export const PDFCheckFlow = "Please check your flow and try again";
|
||||
|
||||
export const PDFErrorTitle = "PDF Output";
|
||||
|
||||
export const PDFLoadError = "Run the flow to see the pdf";
|
||||
|
||||
export const IMGViewConstant = "Expand the view to see the image";
|
||||
|
||||
export const IMGViewErrorMSG =
|
||||
"Run the flow or inform a valid url to see your image";
|
||||
|
||||
export const IMGViewErrorTitle = "Image output";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of code dialog
|
||||
* @constant
|
||||
|
|
@ -510,6 +538,8 @@ export const NOUNS: string[] = [
|
|||
*/
|
||||
export const USER_PROJECTS_HEADER = "My Collection";
|
||||
|
||||
export const DEFAULT_FOLDER = "My Projects";
|
||||
|
||||
/**
|
||||
* Header text for admin page
|
||||
* @constant
|
||||
|
|
@ -688,8 +718,25 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
|
||||
export const priorityFields = new Set(["code", "template"]);
|
||||
|
||||
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
|
||||
export const INPUT_TYPES = new Set([
|
||||
"ChatInput",
|
||||
"TextInput",
|
||||
"KeyPairInput",
|
||||
"JsonInput",
|
||||
"StringListInput",
|
||||
]);
|
||||
export const OUTPUT_TYPES = new Set([
|
||||
"ChatOutput",
|
||||
"TextOutput",
|
||||
"PDFOutput",
|
||||
"ImageOutput",
|
||||
"CSVOutput",
|
||||
"JsonOutput",
|
||||
"KeyPairOutput",
|
||||
"StringListOutput",
|
||||
"RecordsOutput",
|
||||
"TableOutput",
|
||||
]);
|
||||
|
||||
export const CHAT_FIRST_INITIAL_TEXT =
|
||||
"Start a conversation and click the agent's thoughts";
|
||||
|
|
@ -708,8 +755,8 @@ export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
|
|||
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
|
||||
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
|
||||
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
|
||||
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
|
||||
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
|
||||
export const TEXT_INPUT_MODAL_TITLE = "Inputs";
|
||||
export const OUTPUTS_MODAL_TITLE = "Outputs";
|
||||
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
|
||||
export const CHAT_INPUT_PLACEHOLDER =
|
||||
"No chat input variables found. Click to run your flow.";
|
||||
|
|
@ -754,3 +801,7 @@ export const NATIVE_CATEGORIES = [
|
|||
];
|
||||
|
||||
export const SAVE_DEBOUNCE_TIME = 300;
|
||||
|
||||
export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to display right now. Please check back later.`;
|
||||
|
||||
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ function ApiInterceptor() {
|
|||
const interceptor = api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.response?.status === 403 || error.response?.status === 401) {
|
||||
if (
|
||||
error?.response?.status === 403 ||
|
||||
error?.response?.status === 401
|
||||
) {
|
||||
if (!autoLogin) {
|
||||
if (error?.config?.url?.includes("github")) {
|
||||
return Promise.reject(error);
|
||||
|
|
@ -44,7 +47,7 @@ function ApiInterceptor() {
|
|||
}
|
||||
await clearBuildVerticesState(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const isAuthorizedURL = (url) => {
|
||||
|
|
@ -61,10 +64,10 @@ function ApiInterceptor() {
|
|||
const parsedURL = new URL(url);
|
||||
|
||||
const isDomainAllowed = authorizedDomains.some(
|
||||
(domain) => parsedURL.origin === new URL(domain).origin
|
||||
(domain) => parsedURL.origin === new URL(domain).origin,
|
||||
);
|
||||
const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
|
||||
parsedURL.pathname.includes(endpoint)
|
||||
parsedURL.pathname.includes(endpoint),
|
||||
);
|
||||
|
||||
return isDomainAllowed || isEndpointAllowed;
|
||||
|
|
@ -77,6 +80,18 @@ function ApiInterceptor() {
|
|||
// Request interceptor to add access token to every request
|
||||
const requestInterceptor = api.interceptors.request.use(
|
||||
(config) => {
|
||||
const lastUrl = localStorage.getItem("lastUrlCalled");
|
||||
|
||||
if (
|
||||
config?.url === lastUrl &&
|
||||
config?.url !== "/health" &&
|
||||
config?.method === "get"
|
||||
) {
|
||||
return Promise.reject("Duplicate request");
|
||||
}
|
||||
|
||||
localStorage.setItem("lastUrlCalled", config.url ?? "");
|
||||
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
if (accessToken && !isAuthorizedURL(config?.url)) {
|
||||
config.headers["Authorization"] = `Bearer ${accessToken}`;
|
||||
|
|
@ -86,7 +101,7 @@ function ApiInterceptor() {
|
|||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
|
|
@ -118,7 +133,7 @@ function ApiInterceptor() {
|
|||
if (error?.config?.headers) {
|
||||
delete error.config.headers["Authorization"];
|
||||
error.config.headers["Authorization"] = `Bearer ${cookies.get(
|
||||
"access_token_lf"
|
||||
"access_token_lf",
|
||||
)}`;
|
||||
const response = await axios.request(error.config);
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AxiosResponse } from "axios";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { Edge, Node, ReactFlowJsonObject } from "reactflow";
|
||||
import { BASE_URL_API } from "../../constants/constants";
|
||||
import { api } from "../../controllers/API/api";
|
||||
import {
|
||||
|
|
@ -18,6 +19,7 @@ import { UserInputType } from "../../types/components";
|
|||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { StoreComponentResponse } from "../../types/store";
|
||||
import { FlowPoolType } from "../../types/zustand/flow";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
import {
|
||||
APIClassType,
|
||||
BuildStatusTypeAPI,
|
||||
|
|
@ -59,7 +61,7 @@ export async function sendAll(data: sendAllProps) {
|
|||
}
|
||||
|
||||
export async function postValidateCode(
|
||||
code: string
|
||||
code: string,
|
||||
): Promise<AxiosResponse<errorsTypeAPI>> {
|
||||
return await api.post(`${BASE_URL_API}validate/code`, { code });
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ export async function postValidateCode(
|
|||
export async function postValidatePrompt(
|
||||
name: string,
|
||||
template: string,
|
||||
frontend_node: APIClassType
|
||||
frontend_node: APIClassType,
|
||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||
return api.post(`${BASE_URL_API}validate/prompt`, {
|
||||
name,
|
||||
|
|
@ -119,6 +121,7 @@ export async function saveFlowToDatabase(newFlow: {
|
|||
description: string;
|
||||
style?: FlowStyleType;
|
||||
is_component?: boolean;
|
||||
folder_id?: string;
|
||||
}): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}flows/`, {
|
||||
|
|
@ -126,6 +129,7 @@ export async function saveFlowToDatabase(newFlow: {
|
|||
data: newFlow.data,
|
||||
description: newFlow.description,
|
||||
is_component: newFlow.is_component,
|
||||
folder_id: newFlow.folder_id === "" ? null : newFlow.folder_id,
|
||||
});
|
||||
|
||||
if (response.status !== 201) {
|
||||
|
|
@ -145,13 +149,14 @@ export async function saveFlowToDatabase(newFlow: {
|
|||
* @throws Will throw an error if the update fails.
|
||||
*/
|
||||
export async function updateFlowInDatabase(
|
||||
updatedFlow: FlowType
|
||||
updatedFlow: FlowType,
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
|
||||
name: updatedFlow.name,
|
||||
data: updatedFlow.data,
|
||||
description: updatedFlow.description,
|
||||
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
|
||||
});
|
||||
|
||||
if (response?.status !== 200) {
|
||||
|
|
@ -321,7 +326,7 @@ export async function getHealth() {
|
|||
*
|
||||
*/
|
||||
export async function getBuildStatus(
|
||||
flowId: string
|
||||
flowId: string,
|
||||
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
|
||||
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
|
||||
}
|
||||
|
|
@ -334,7 +339,7 @@ export async function getBuildStatus(
|
|||
*
|
||||
*/
|
||||
export async function postBuildInit(
|
||||
flow: FlowType
|
||||
flow: FlowType,
|
||||
): Promise<AxiosResponse<InitTypeAPI>> {
|
||||
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
|
||||
}
|
||||
|
|
@ -350,7 +355,7 @@ export async function postBuildInit(
|
|||
*/
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<AxiosResponse<UploadFileTypeAPI>> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
|
@ -359,7 +364,7 @@ export async function uploadFile(
|
|||
|
||||
export async function postCustomComponent(
|
||||
code: string,
|
||||
apiClass: APIClassType
|
||||
apiClass: APIClassType,
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
// let template = apiClass.template;
|
||||
return await api.post(`${BASE_URL_API}custom_component`, {
|
||||
|
|
@ -372,7 +377,7 @@ export async function postCustomComponentUpdate(
|
|||
code: string,
|
||||
template: APITemplateType,
|
||||
field: string,
|
||||
field_value: any
|
||||
field_value: any,
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
||||
code,
|
||||
|
|
@ -394,7 +399,7 @@ export async function onLogin(user: LoginType) {
|
|||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
|
|
@ -406,9 +411,11 @@ export async function onLogin(user: LoginType) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function autoLogin() {
|
||||
export async function autoLogin(abortSignal) {
|
||||
try {
|
||||
const response = await api.get(`${BASE_URL_API}auto_login`);
|
||||
const response = await api.get(`${BASE_URL_API}auto_login`, {
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.data;
|
||||
|
|
@ -454,11 +461,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
|
|||
|
||||
export async function getUsersPage(
|
||||
skip: number,
|
||||
limit: number
|
||||
limit: number,
|
||||
): Promise<Array<Users>> {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
|
||||
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -495,7 +502,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
|
|||
try {
|
||||
const res = await api.patch(
|
||||
`${BASE_URL_API}users/${user_id}/reset-password`,
|
||||
user
|
||||
user,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -569,7 +576,7 @@ export async function saveFlowStore(
|
|||
last_tested_version?: string;
|
||||
},
|
||||
tags: string[],
|
||||
publicFlow = false
|
||||
publicFlow = false,
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}store/components/`, {
|
||||
|
|
@ -698,7 +705,7 @@ export async function postStoreComponents(component: Component) {
|
|||
export async function getComponent(component_id: string) {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`${BASE_URL_API}store/components/${component_id}`
|
||||
`${BASE_URL_API}store/components/${component_id}`,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -713,7 +720,7 @@ export async function searchComponent(
|
|||
page?: number | null,
|
||||
limit?: number | null,
|
||||
status?: string | null,
|
||||
tags?: string[]
|
||||
tags?: string[],
|
||||
): Promise<StoreComponentResponse | undefined> {
|
||||
try {
|
||||
let url = `${BASE_URL_API}store/components/`;
|
||||
|
|
@ -825,7 +832,7 @@ export async function updateFlowStore(
|
|||
},
|
||||
tags: string[],
|
||||
publicFlow = false,
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
|
||||
|
|
@ -860,13 +867,14 @@ export async function requestLogout() {
|
|||
}
|
||||
|
||||
export async function getGlobalVariables(): Promise<{
|
||||
[key: string]: { id: string; type: string };
|
||||
[key: string]: { id: string; type: string; default_fields: string[] };
|
||||
}> {
|
||||
const globalVariables = {};
|
||||
(await api.get(`${BASE_URL_API}variables/`)).data.forEach((element) => {
|
||||
globalVariables[element.name] = {
|
||||
id: element.id,
|
||||
type: element.type,
|
||||
default_fields: element.default_fields,
|
||||
};
|
||||
});
|
||||
return globalVariables;
|
||||
|
|
@ -876,47 +884,79 @@ export async function registerGlobalVariable({
|
|||
name,
|
||||
value,
|
||||
type,
|
||||
default_fields = [],
|
||||
}: {
|
||||
name: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
default_fields?: string[];
|
||||
}): Promise<AxiosResponse<{ name: string; id: string; type: string }>> {
|
||||
return await api.post(`${BASE_URL_API}variables/`, {
|
||||
name,
|
||||
value,
|
||||
type,
|
||||
});
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}variables/`, {
|
||||
name,
|
||||
value,
|
||||
type,
|
||||
default_fields: default_fields,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGlobalVariable(id: string) {
|
||||
api.delete(`${BASE_URL_API}variables/${id}`);
|
||||
try {
|
||||
const response = await api.delete(`${BASE_URL_API}variables/${id}`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGlobalVariable(
|
||||
name: string,
|
||||
value: string,
|
||||
id: string
|
||||
id: string,
|
||||
) {
|
||||
api.patch(`${BASE_URL_API}variables/${id}`, {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
try {
|
||||
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVerticesOrder(
|
||||
flowId: string,
|
||||
startNodeId?: string | null,
|
||||
stopNodeId?: string | null
|
||||
stopNodeId?: string | null,
|
||||
nodes?: Node[],
|
||||
Edges?: Edge[],
|
||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||
// nodeId is optional and is a query parameter
|
||||
// if nodeId is not provided, the API will return all vertices
|
||||
const config = {};
|
||||
const config: AxiosRequestConfig<any> = {};
|
||||
if (stopNodeId) {
|
||||
config["params"] = { stop_component_id: stopNodeId };
|
||||
} else if (startNodeId) {
|
||||
config["params"] = { start_component_id: startNodeId };
|
||||
}
|
||||
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
|
||||
const data = {
|
||||
data: {},
|
||||
};
|
||||
if (nodes && Edges) {
|
||||
data["data"]["nodes"] = nodes;
|
||||
data["data"]["edges"] = Edges;
|
||||
}
|
||||
return await api.post(
|
||||
`${BASE_URL_API}build/${flowId}/vertices`,
|
||||
data,
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
export async function postBuildVertex(
|
||||
|
|
@ -955,9 +995,47 @@ export async function getFlowPool({
|
|||
}
|
||||
|
||||
export async function deleteFlowPool(
|
||||
flowId: string
|
||||
flowId: string,
|
||||
): Promise<AxiosResponse<any>> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: flowId };
|
||||
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
|
||||
}
|
||||
|
||||
export async function multipleDeleteFlowsComponents(
|
||||
flowIds: string[],
|
||||
): Promise<AxiosResponse<any>> {
|
||||
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
|
||||
flow_ids: flowIds,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTransactionTable(
|
||||
id: string,
|
||||
mode: "intersection" | "union",
|
||||
params = {},
|
||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: id };
|
||||
if (params) {
|
||||
config["params"] = { ...config["params"], ...params };
|
||||
}
|
||||
const rows = await api.get(`${BASE_URL_API}monitor/transactions`, config);
|
||||
const columns = extractColumnsFromRows(rows.data, mode);
|
||||
return { rows: rows.data, columns };
|
||||
}
|
||||
|
||||
export async function getMessagesTable(
|
||||
id: string,
|
||||
mode: "intersection" | "union",
|
||||
params = {},
|
||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: id };
|
||||
if (params) {
|
||||
config["params"] = { ...config["params"], ...params };
|
||||
}
|
||||
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
|
||||
const columns = extractColumnsFromRows(rows.data, mode);
|
||||
return { rows: rows.data, columns };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||
import DictComponent from "../../../../components/dictComponent";
|
||||
import Dropdown from "../../../../components/dropdownComponent";
|
||||
|
|
@ -13,25 +12,21 @@ import InputListComponent from "../../../../components/inputListComponent";
|
|||
import IntComponent from "../../../../components/intComponent";
|
||||
import KeypairListComponent from "../../../../components/keypairListComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import ShadTooltip from "../../../../components/shadTooltipComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
||||
import {
|
||||
INPUT_HANDLER_HOVER,
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
OUTPUT_HANDLER_HOVER,
|
||||
TOOLTIP_EMPTY,
|
||||
} from "../../../../constants/constants";
|
||||
import { Case } from "../../../../shared/components/caseComponent";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
import {
|
||||
APIClassType,
|
||||
ResponseErrorDetailAPI,
|
||||
ResponseErrorTypeAPI,
|
||||
} from "../../../../types/api";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { ParameterComponentType } from "../../../../types/components";
|
||||
import {
|
||||
debouncedHandleUpdateValues,
|
||||
|
|
@ -44,12 +39,13 @@ import {
|
|||
isValidConnection,
|
||||
scapedJSONStringfy,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import {
|
||||
nodeColors,
|
||||
nodeIconsLucide,
|
||||
nodeNames,
|
||||
} from "../../../../utils/styleUtils";
|
||||
import { nodeColors } from "../../../../utils/styleUtils";
|
||||
import { classNames, groupByFamily } from "../../../../utils/utils";
|
||||
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
||||
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
||||
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
||||
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
||||
import TooltipRenderComponent from "../tooltipRenderComponent";
|
||||
|
||||
export default function ParameterComponent({
|
||||
left,
|
||||
|
|
@ -75,159 +71,69 @@ export default function ParameterComponent({
|
|||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const myData = useTypesStore((state) => state.data);
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
const flow = currentFlow?.data?.nodes ?? null;
|
||||
|
||||
const groupedEdge = useRef(null);
|
||||
|
||||
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||
|
||||
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
|
||||
data,
|
||||
name,
|
||||
takeSnapshot,
|
||||
handleUpdateValues,
|
||||
debouncedHandleUpdateValues,
|
||||
setNode,
|
||||
renderTooltips,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
);
|
||||
|
||||
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
|
||||
data,
|
||||
name,
|
||||
takeSnapshot,
|
||||
setNode,
|
||||
updateNodeInternals,
|
||||
renderTooltips,
|
||||
);
|
||||
|
||||
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
|
||||
useHandleRefreshButtonPress(setIsLoading, setNode, renderTooltips);
|
||||
|
||||
let disabled =
|
||||
edges.some(
|
||||
(edge) =>
|
||||
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id)
|
||||
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
|
||||
) ?? false;
|
||||
|
||||
const myData = useTypesStore((state) => state.data);
|
||||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const handleRefreshButtonPress = async (name, data) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
handleRefreshButtonPressHook(name, data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (
|
||||
(data.node?.template[name]?.real_time_refresh ||
|
||||
data.node?.template[name]?.refresh_button) &&
|
||||
// options can be undefined but not an empty array
|
||||
(data.node?.template[name]?.options?.length ?? 0) === 0
|
||||
) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
useFetchDataOnMount(
|
||||
data,
|
||||
name,
|
||||
handleUpdateValues,
|
||||
setNode,
|
||||
renderTooltips,
|
||||
setIsLoading,
|
||||
);
|
||||
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
const handleOnNewValue = async (
|
||||
newValue: string | string[] | boolean | Object[]
|
||||
newValue: string | string[] | boolean | Object[],
|
||||
skipSnapshot: boolean | undefined = false,
|
||||
): Promise<void> => {
|
||||
if (data.node!.template[name].value !== newValue) {
|
||||
takeSnapshot();
|
||||
}
|
||||
const shouldUpdate =
|
||||
data.node?.template[name].real_time_refresh &&
|
||||
!data.node?.template[name].refresh_button &&
|
||||
data.node!.template[name].value !== newValue;
|
||||
|
||||
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
|
||||
let newTemplate;
|
||||
if (shouldUpdate) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
newTemplate = await debouncedHandleUpdateValues(name, data);
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorTypeAPI;
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail.error ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
// this de
|
||||
}
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
|
||||
if (data.node?.template[name].real_time_refresh && newTemplate) {
|
||||
newNode.data.node.template = newTemplate;
|
||||
} else newNode.data.node.template[name].value = newValue;
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
renderTooltips();
|
||||
handleOnNewValueHook(newValue, skipSnapshot);
|
||||
};
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
|
||||
if (!data.node) return;
|
||||
if (data.node!.template[name].value !== code) {
|
||||
takeSnapshot();
|
||||
}
|
||||
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
|
||||
renderTooltips();
|
||||
handleNodeClassHook(newNodeClass, code);
|
||||
};
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
infoHtml.current = (
|
||||
|
|
@ -248,88 +154,7 @@ export default function ParameterComponent({
|
|||
if (groupedObj && groupedObj.length > 0) {
|
||||
//@ts-ignore
|
||||
refHtml.current = groupedObj.map((item, index) => {
|
||||
const Icon: any =
|
||||
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
data-testid={`available-${left ? "input" : "output"}-${
|
||||
item.family
|
||||
}`}
|
||||
>
|
||||
{index === 0 && (
|
||||
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
|
||||
)}
|
||||
<span
|
||||
key={index}
|
||||
className={classNames(
|
||||
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-5 w-5"
|
||||
style={{
|
||||
color: nodeColors[item.family],
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="h-5 w-5"
|
||||
strokeWidth={1.5}
|
||||
style={{
|
||||
color: nodeColors[item.family] ?? nodeColors.unknown,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="ps-2 text-xs text-foreground"
|
||||
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
|
||||
>
|
||||
{nodeNames[item.family] ?? "Other"}{" "}
|
||||
{item?.display_name && item?.display_name?.length > 0 ? (
|
||||
<span
|
||||
className="text-xs"
|
||||
data-testid={`tooltip-${item?.display_name}`}
|
||||
>
|
||||
{" "}
|
||||
{item.display_name === "" ? "" : " - "}
|
||||
{item.display_name.split(", ").length > 2
|
||||
? item.display_name.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index ===
|
||||
item.display_name.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.display_name}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="text-xs"
|
||||
data-testid={`tooltip-${item?.type}`}
|
||||
>
|
||||
{" "}
|
||||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return <TooltipRenderComponent index={index} item={item} left={left} />;
|
||||
});
|
||||
} else {
|
||||
//@ts-ignore
|
||||
|
|
@ -338,6 +163,7 @@ export default function ParameterComponent({
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If optionalHandle is an empty list, then it is not an optional handle
|
||||
if (optionalHandle && optionalHandle.length === 0) {
|
||||
optionalHandle = null;
|
||||
|
|
@ -346,6 +172,9 @@ export default function ParameterComponent({
|
|||
useEffect(() => {
|
||||
renderTooltips();
|
||||
}, [tooltipTitle, flow]);
|
||||
|
||||
console.log(left === true && type === "dict");
|
||||
|
||||
return !showNode ? (
|
||||
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||
<></>
|
||||
|
|
@ -380,7 +209,7 @@ export default function ParameterComponent({
|
|||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background",
|
||||
!showNode ? "mt-0" : ""
|
||||
!showNode ? "mt-0" : "",
|
||||
)}
|
||||
style={{
|
||||
borderColor: color ?? nodeColors.unknown,
|
||||
|
|
@ -411,11 +240,12 @@ export default function ParameterComponent({
|
|||
(left ? "" : " justify-end")
|
||||
}
|
||||
>
|
||||
{!left && data.node?.frozen && (
|
||||
<Case condition={left && data.node?.frozen}>
|
||||
<div className="pr-1">
|
||||
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
|
||||
</div>
|
||||
)}
|
||||
</Case>
|
||||
|
||||
{proxy ? (
|
||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
|
|
@ -461,45 +291,37 @@ export default function ParameterComponent({
|
|||
}`}
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
key={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
|
||||
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, nodes, edges)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "-ml-0.5 " : "-mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
left ? "-ml-0.5" : "-mr-0.5",
|
||||
"h-3 w-3 rounded-full border-2 bg-background",
|
||||
)}
|
||||
style={{
|
||||
borderColor: color ?? nodeColors.unknown,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
style={{ borderColor: color ?? nodeColors.unknown }}
|
||||
onClick={() => setFilterEdge(groupedEdge.current)}
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{left === true &&
|
||||
type === "str" &&
|
||||
!data.node?.template[name].options ? (
|
||||
<Case
|
||||
condition={
|
||||
left === true &&
|
||||
type === "str" &&
|
||||
!data.node?.template[name]?.options
|
||||
}
|
||||
>
|
||||
<div className="w-full">
|
||||
{data.node?.template[name].list ? (
|
||||
<Case condition={data.node?.template[name]?.list}>
|
||||
<div
|
||||
className={
|
||||
// Commenting this out until we have a better
|
||||
// way to display
|
||||
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
|
||||
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
|
||||
"flex-grow"
|
||||
}
|
||||
>
|
||||
|
|
@ -507,39 +329,27 @@ export default function ParameterComponent({
|
|||
componentName={name}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
!data.node!.template[name]?.value ||
|
||||
data.node!.template[name]?.value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
: data.node!.template[name]?.value
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
{/* {data.node?.template[name].refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
) : data.node?.template[name].multiline ? (
|
||||
</Case>
|
||||
<Case condition={data.node?.template[name]?.multiline}>
|
||||
<div className="mt-2 flex w-full flex-col ">
|
||||
<div className="flex-grow">
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
value={data.node!.template[name]?.value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + data.node.template[name].name}
|
||||
data-testid={"textarea-" + data.node.template[name].name}
|
||||
id={"textarea-" + data.node!.template[name]?.name}
|
||||
data-testid={"textarea-" + data.node!.template[name]?.name}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
{data.node?.template[name]?.refresh_button && (
|
||||
<div className="flex-grow">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
|
|
@ -547,7 +357,7 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name].refresh_button_text ??
|
||||
data.node?.template[name]?.refresh_button_text ??
|
||||
"Refresh"
|
||||
}
|
||||
className="extra-side-bar-buttons mt-1"
|
||||
|
|
@ -557,12 +367,18 @@ export default function ParameterComponent({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
</Case>
|
||||
<Case
|
||||
condition={
|
||||
!data.node?.template[name]?.multiline &&
|
||||
!data.node?.template[name]?.list
|
||||
}
|
||||
>
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div
|
||||
className={
|
||||
"flex-grow " +
|
||||
(data.node?.template[name].refresh_button ? "w-5/6" : "")
|
||||
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
|
||||
}
|
||||
>
|
||||
<InputGlobalComponent
|
||||
|
|
@ -582,7 +398,7 @@ export default function ParameterComponent({
|
|||
data={data}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
{data.node?.template[name]?.refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
|
|
@ -590,7 +406,7 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name].refresh_button_text ??
|
||||
data.node?.template[name]?.refresh_button_text ??
|
||||
"Refresh"
|
||||
}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
|
|
@ -600,52 +416,61 @@ export default function ParameterComponent({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Case>
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "bool"}>
|
||||
<div className="mt-2 w-full">
|
||||
<ToggleShadComponent
|
||||
id={"toggle-" + name}
|
||||
disabled={disabled}
|
||||
enabled={data.node?.template[name].value ?? false}
|
||||
enabled={data.node?.template[name]?.value ?? false}
|
||||
setEnabled={handleOnNewValue}
|
||||
size="large"
|
||||
editNode={false}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "float"}>
|
||||
<div className="mt-2 w-full">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
rangeSpec={data.node?.template[name].rangeSpec}
|
||||
value={data.node?.template[name]?.value ?? ""}
|
||||
rangeSpec={data.node?.template[name]?.rangeSpec}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
(data.node?.template[name].options ||
|
||||
data.node?.template[name]?.real_time_refresh) ? (
|
||||
// TODO: Improve CSS
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
left === true &&
|
||||
type === "str" &&
|
||||
(data.node?.template[name]?.options ||
|
||||
data.node?.template[name]?.real_time_refresh)
|
||||
}
|
||||
>
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div className="w-5/6 flex-grow">
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
options={data.node.template[name].options}
|
||||
options={data.node!.template[name]?.options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value}
|
||||
value={data.node!.template[name]?.value}
|
||||
id={"dropdown-" + name}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
{data.node?.template[name]?.refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
button_text={data.node?.template[name].refresh_button_text}
|
||||
button_text={data.node?.template[name]?.refresh_button_text}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
|
|
@ -653,46 +478,54 @@ export default function ParameterComponent({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "code" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "code"}>
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
data.node?.flow && data.node.template[name].dynamic
|
||||
data.node?.flow && data.node.template[name]?.dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={data.node?.template[name].dynamic ?? false}
|
||||
dynamic={data.node?.template[name]?.dynamic ?? false}
|
||||
setNodeClass={handleNodeClass}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
value={data.node?.template[name]?.value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"code-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "file" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "file"}>
|
||||
<div className="mt-2 w-full">
|
||||
<InputFileComponent
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
value={data.node?.template[name]?.value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node?.template[name].fileTypes}
|
||||
fileTypes={data.node?.template[name]?.fileTypes}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node!.template[name].file_path = filePath;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : left === true && type === "int" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "int"}>
|
||||
<div className="mt-2 w-full">
|
||||
<IntComponent
|
||||
rangeSpec={data.node?.template[name].rangeSpec}
|
||||
rangeSpec={data.node?.template[name]?.rangeSpec}
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
value={data.node?.template[name]?.value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"int-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "prompt"}>
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
readonly={data.node?.flow ? true : false}
|
||||
|
|
@ -700,57 +533,59 @@ export default function ParameterComponent({
|
|||
setNodeClass={handleNodeClass}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
value={data.node?.template[name]?.value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"prompt-input-" + name}
|
||||
data-testid={"prompt-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "NestedDict" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "NestedDict"}>
|
||||
<div className="mt-2 w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
!data.node!.template[name].value ||
|
||||
data.node!.template[name].value?.toString() === "{}"
|
||||
!data.node!.template[name]?.value ||
|
||||
data.node!.template[name]?.value?.toString() === "{}"
|
||||
? {
|
||||
yourkey: "value",
|
||||
// yourkey: "value",
|
||||
}
|
||||
: data.node!.template[name].value
|
||||
: data.node!.template[name]?.value
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
id="div-dict-input"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "dict" ? (
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "dict"}>
|
||||
<div className="mt-2 w-full">
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
data.node!.template[name].value?.length === 0 ||
|
||||
!data.node!.template[name].value
|
||||
data.node!.template[name]?.value?.length === 0 ||
|
||||
!data.node!.template[name]?.value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(data.node!.template[name].value)
|
||||
: convertObjToArray(data.node!.template[name]?.value, type!)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
// if data.node?.template[name].list is true, then the value is an array of objects
|
||||
// if data.node?.template[name]?.list is true, then the value is an array of objects
|
||||
// else we need to get the first object of the array
|
||||
|
||||
if (data.node?.template[name].list) {
|
||||
if (data.node?.template[name]?.list) {
|
||||
handleOnNewValue(valueToNumbers);
|
||||
} else handleOnNewValue(valueToNumbers[0]);
|
||||
}}
|
||||
isList={data.node?.template[name].list ?? false}
|
||||
isList={data.node?.template[name]?.list ?? false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Case>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from "react";
|
||||
import {
|
||||
INPUT_HANDLER_HOVER,
|
||||
OUTPUT_HANDLER_HOVER,
|
||||
} from "../../../../constants/constants";
|
||||
import {
|
||||
nodeColors,
|
||||
nodeIconsLucide,
|
||||
nodeNames,
|
||||
} from "../../../../utils/styleUtils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
|
||||
const TooltipRenderComponent = ({ item, index, left }) => {
|
||||
const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
|
||||
>
|
||||
{index === 0 && (
|
||||
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
|
||||
)}
|
||||
<span
|
||||
key={index}
|
||||
className={classNames(
|
||||
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-5 w-5"
|
||||
style={{
|
||||
color: nodeColors[item.family],
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="h-5 w-5"
|
||||
strokeWidth={1.5}
|
||||
style={{
|
||||
color: nodeColors[item.family] ?? nodeColors.unknown,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="ps-2 text-xs text-foreground"
|
||||
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
|
||||
>
|
||||
{nodeNames[item.family] ?? "Other"}{" "}
|
||||
{item?.display_name && item?.display_name?.length > 0 ? (
|
||||
<span
|
||||
className="text-xs"
|
||||
data-testid={`tooltip-${item?.display_name}`}
|
||||
>
|
||||
{" "}
|
||||
{item.display_name === "" ? "" : " - "}
|
||||
{item.display_name.split(", ").length > 2
|
||||
? item.display_name.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index === item.display_name.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.display_name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
|
||||
{" "}
|
||||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TooltipRenderComponent;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
|
|
@ -27,8 +27,10 @@ import { validationStatusType } from "../../types/components";
|
|||
import { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, cn, getFieldTitle, sortFields } from "../../utils/utils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import getFieldTitle from "../utils/get-field-title";
|
||||
import sortFields from "../utils/sort-fields";
|
||||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
|
|
@ -54,14 +56,14 @@ export default function GenericNode({
|
|||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!
|
||||
data.node?.description!,
|
||||
);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
|
|
@ -118,7 +120,7 @@ export default function GenericNode({
|
|||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated]
|
||||
[data.id, data.node, setNode, setIsOutdated],
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
|
|
@ -309,7 +311,7 @@ export default function GenericNode({
|
|||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
|
|
@ -333,15 +335,16 @@ export default function GenericNode({
|
|||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus
|
||||
validationStatus,
|
||||
);
|
||||
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
const nodeSizeClass = getNodeSizeClass(showNode);
|
||||
return classNames(
|
||||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
specificClassFromBuildStatus
|
||||
specificClassFromBuildStatus,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -389,7 +392,6 @@ export default function GenericNode({
|
|||
isOutdated,
|
||||
selected,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{memoizedNodeToolbarComponent}
|
||||
|
|
@ -398,7 +400,7 @@ export default function GenericNode({
|
|||
selected,
|
||||
showNode,
|
||||
buildStatus,
|
||||
validationStatus
|
||||
validationStatus,
|
||||
)}
|
||||
>
|
||||
{data.node?.beta && showNode && (
|
||||
|
|
@ -543,7 +545,7 @@ export default function GenericNode({
|
|||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
templateField,
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
|
|
@ -571,7 +573,7 @@ export default function GenericNode({
|
|||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)
|
||||
),
|
||||
)}
|
||||
<ParameterComponent
|
||||
key={scapedJSONStringfy({
|
||||
|
|
@ -651,7 +653,11 @@ export default function GenericNode({
|
|||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
</div>
|
||||
|
|
@ -724,7 +730,7 @@ export default function GenericNode({
|
|||
!data.node?.description) &&
|
||||
nameEditable
|
||||
? "font-light italic"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
onDoubleClick={(e) => {
|
||||
setInputDescription(true);
|
||||
|
|
@ -786,13 +792,13 @@ export default function GenericNode({
|
|||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
templateField,
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
data.node?.template[templateField].input_types?.join(
|
||||
"\n"
|
||||
"\n",
|
||||
) ?? data.node?.template[templateField].type
|
||||
}
|
||||
required={data.node!.template[templateField].required}
|
||||
|
|
@ -819,7 +825,7 @@ export default function GenericNode({
|
|||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
||||
"flex-max-width justify-center"
|
||||
"flex-max-width justify-center",
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useEffect } from "react";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
|
||||
const useFetchDataOnMount = (
|
||||
data,
|
||||
name,
|
||||
handleUpdateValues,
|
||||
setNode,
|
||||
renderTooltips,
|
||||
setIsLoading,
|
||||
) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (
|
||||
(data.node?.template[name]?.real_time_refresh ||
|
||||
data.node?.template[name]?.refresh_button) &&
|
||||
// options can be undefined but not an empty array
|
||||
(data.node?.template[name]?.options?.length ?? 0) === 0
|
||||
) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, []); // Empty dependency array ensures that this effect runs only once, on mount
|
||||
};
|
||||
|
||||
export default useFetchDataOnMount;
|
||||
75
src/frontend/src/customNodes/hooks/use-handle-new-value.tsx
Normal file
75
src/frontend/src/customNodes/hooks/use-handle-new-value.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ResponseErrorTypeAPI } from "../../types/api";
|
||||
|
||||
const useHandleOnNewValue = (
|
||||
data,
|
||||
name,
|
||||
takeSnapshot,
|
||||
handleUpdateValues,
|
||||
debouncedHandleUpdateValues,
|
||||
setNode,
|
||||
renderTooltips,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const handleOnNewValue = async (newValue, skipSnapshot = false) => {
|
||||
const nodeTemplate = data.node!.template[name];
|
||||
const currentValue = nodeTemplate.value;
|
||||
|
||||
if (currentValue !== newValue && !skipSnapshot) {
|
||||
takeSnapshot();
|
||||
}
|
||||
|
||||
const shouldUpdate =
|
||||
data.node?.template[name].real_time_refresh &&
|
||||
!data.node?.template[name].refresh_button &&
|
||||
currentValue !== newValue;
|
||||
|
||||
const typeToDebounce = nodeTemplate.type;
|
||||
|
||||
nodeTemplate.value = newValue;
|
||||
|
||||
let newTemplate;
|
||||
if (shouldUpdate) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (["int"].includes(typeToDebounce)) {
|
||||
newTemplate = await handleUpdateValues(name, data);
|
||||
} else {
|
||||
newTemplate = await debouncedHandleUpdateValues(name, data);
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorTypeAPI;
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail.error ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setNode(data.id, (oldNode) => {
|
||||
const newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
|
||||
if (data.node?.template[name].real_time_refresh && newTemplate) {
|
||||
newNode.data.node.template = newTemplate;
|
||||
} else {
|
||||
newNode.data.node.template[name].value = newValue;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
renderTooltips();
|
||||
};
|
||||
|
||||
return { handleOnNewValue };
|
||||
};
|
||||
|
||||
export default useHandleOnNewValue;
|
||||
40
src/frontend/src/customNodes/hooks/use-handle-node-class.tsx
Normal file
40
src/frontend/src/customNodes/hooks/use-handle-node-class.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
|
||||
const useHandleNodeClass = (
|
||||
data,
|
||||
name,
|
||||
takeSnapshot,
|
||||
setNode,
|
||||
updateNodeInternals,
|
||||
renderTooltips,
|
||||
) => {
|
||||
const handleNodeClass = (newNodeClass, code) => {
|
||||
if (!data.node) return;
|
||||
if (data.node!.template[name].value !== code) {
|
||||
takeSnapshot();
|
||||
}
|
||||
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
|
||||
renderTooltips();
|
||||
};
|
||||
|
||||
return { handleNodeClass };
|
||||
};
|
||||
|
||||
export default useHandleNodeClass;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
import { handleUpdateValues } from "../../utils/parameterUtils";
|
||||
|
||||
const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const handleRefreshButtonPress = async (name, data) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
};
|
||||
|
||||
return { handleRefreshButtonPress };
|
||||
};
|
||||
|
||||
export default useHandleRefreshButtonPress;
|
||||
10
src/frontend/src/customNodes/utils/get-field-title.tsx
Normal file
10
src/frontend/src/customNodes/utils/get-field-title.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { APITemplateType } from "../../types/api";
|
||||
|
||||
export default function getFieldTitle(
|
||||
template: APITemplateType,
|
||||
templateField: string,
|
||||
): string {
|
||||
return template[templateField].display_name
|
||||
? template[templateField].display_name!
|
||||
: template[templateField].name ?? templateField;
|
||||
}
|
||||
40
src/frontend/src/customNodes/utils/sort-fields.tsx
Normal file
40
src/frontend/src/customNodes/utils/sort-fields.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { priorityFields } from "../../constants/constants";
|
||||
|
||||
export default function sortFields(a, b, fieldOrder) {
|
||||
// Early return for empty fields
|
||||
if (!a && !b) return 0;
|
||||
if (!a) return 1;
|
||||
if (!b) return -1;
|
||||
|
||||
// Normalize the case to ensure case-insensitive comparison
|
||||
const normalizedFieldA = a.toLowerCase();
|
||||
const normalizedFieldB = b.toLowerCase();
|
||||
|
||||
const aIsPriority = priorityFields.has(normalizedFieldA);
|
||||
const bIsPriority = priorityFields.has(normalizedFieldB);
|
||||
|
||||
// Sort by priority
|
||||
if (aIsPriority && !bIsPriority) return -1;
|
||||
if (!aIsPriority && bIsPriority) return 1;
|
||||
|
||||
// Check if either field is in the fieldOrder array
|
||||
const indexOfA = fieldOrder.indexOf(normalizedFieldA);
|
||||
const indexOfB = fieldOrder.indexOf(normalizedFieldB);
|
||||
|
||||
// If both fields are in fieldOrder, sort by their order in the array
|
||||
if (indexOfA !== -1 && indexOfB !== -1) {
|
||||
return indexOfA - indexOfB;
|
||||
}
|
||||
|
||||
// If only one of the fields is in fieldOrder, that field comes first
|
||||
if (indexOfA !== -1) {
|
||||
return -1;
|
||||
}
|
||||
if (indexOfB !== -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Default case for fields not in priorityFields and not found in fieldOrder
|
||||
// You might want to sort them alphabetically or in another specific manner
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
17
src/frontend/src/icons/Couchbase/Couchbase.jsx
Normal file
17
src/frontend/src/icons/Couchbase/Couchbase.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const SvgCouchbaseIcon = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ED2226"
|
||||
d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgCouchbaseIcon;
|
||||
1
src/frontend/src/icons/Couchbase/couchbase.svg
Normal file
1
src/frontend/src/icons/Couchbase/couchbase.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" preserveAspectRatio="xMidYMid" viewBox="0 0 256 256" id="couchbase"><path fill="#ED2226" d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"></path></svg>
|
||||
|
After Width: | Height: | Size: 720 B |
9
src/frontend/src/icons/Couchbase/index.tsx
Normal file
9
src/frontend/src/icons/Couchbase/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgCouchbaseIcon from "./Couchbase";
|
||||
|
||||
export const CouchbaseIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgCouchbaseIcon ref={ref} {...props} />;
|
||||
});
|
||||
|
|
@ -56,7 +56,11 @@ export const GradientGroup = (props) => {
|
|||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
|
||||
<ForwardedIconComponent
|
||||
name="Combine"
|
||||
stroke={`${props.disabled ? "#64748B" : "url(#grad3)"}`}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
22
src/frontend/src/icons/Groq/GroqLogo.jsx
Normal file
22
src/frontend/src/icons/Groq/GroqLogo.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const SvgGroqLogo = ({ color, ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="groq logo"
|
||||
role="img"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
class="fill-foreground"
|
||||
>
|
||||
<path
|
||||
fill="#F55036"
|
||||
d="M24 12c0 6.627-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0s12 5.373 12 12Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M12.002 6a4.118 4.118 0 0 0-4.118 4.108 4.118 4.118 0 0 0 4.118 4.109h1.354v-1.541h-1.354a2.574 2.574 0 0 1-2.574-2.568 2.574 2.574 0 0 1 2.574-2.568c1.42 0 2.58 1.152 2.58 2.568v3.784a2.58 2.58 0 0 1-2.555 2.567 2.558 2.558 0 0 1-1.791-.752l-1.092 1.09a4.095 4.095 0 0 0 2.855 1.202l.028.001h.029a4.118 4.118 0 0 0 4.061-4.09l.002-3.903A4.119 4.119 0 0 0 12.002 6Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
export default SvgGroqLogo;
|
||||
38
src/frontend/src/icons/Groq/GroqLogo.svg
Normal file
38
src/frontend/src/icons/Groq/GroqLogo.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<svg version="1.1" id="Layer_1" x="0px" y="0px" width="152px" height="55.5px" viewBox="0 32.25 152 55.5" enable-background="new 0 32.25 152 55.5" xml:space="preserve">
|
||||
<title>
|
||||
groq_logo
|
||||
</title>
|
||||
<g id="Layer_2">
|
||||
<g id="Layer_1-2">
|
||||
<path d="M84.848,34.137c-9.798,0-17.769,7.971-17.769,17.77s7.971,17.769,17.769,17.769s17.77-7.971,17.77-17.769
|
||||
S94.645,34.137,84.848,34.137z M84.848,63.013c-6.124,0-11.106-4.983-11.106-11.106s4.982-11.106,11.106-11.106
|
||||
c6.124,0,11.106,4.982,11.106,11.106S90.973,63.013,84.848,63.013z">
|
||||
</path>
|
||||
<path d="M60.315,34.206c-0.607-0.068-1.217-0.104-1.827-0.108c-0.304,0-0.595,0.009-0.893,0.014s-0.594,0.033-0.891,0.051
|
||||
c-1.197,0.094-2.382,0.299-3.541,0.611c-2.329,0.629-4.574,1.723-6.515,3.277c-1.97,1.57-3.548,3.575-4.611,5.859
|
||||
c-0.53,1.138-0.921,2.336-1.165,3.567c-0.121,0.608-0.21,1.222-0.266,1.84c-0.02,0.307-0.055,0.615-0.059,0.921l-0.011,0.459
|
||||
l-0.005,0.23v0.19l0.015,5.951l0.015,5.951l0.041,5.95h6.664l0.042-5.95l0.015-5.952l0.015-5.951v-0.182l0.005-0.142l0.008-0.285
|
||||
c0-0.191,0.028-0.375,0.039-0.564c0.036-0.37,0.091-0.738,0.165-1.102c0.146-0.716,0.374-1.413,0.678-2.077
|
||||
c0.613-1.332,1.528-2.502,2.673-3.419c1.156-0.932,2.541-1.628,4.038-2.042c0.757-0.207,1.532-0.344,2.314-0.408
|
||||
c0.198-0.011,0.395-0.03,0.594-0.037c0.199-0.007,0.402-0.013,0.595-0.012c0.383,0,0.76,0.025,1.142,0.06
|
||||
c1.518,0.153,2.989,0.619,4.318,1.368l3.326-5.776C65.108,35.263,62.753,34.484,60.315,34.206z">
|
||||
</path>
|
||||
<path d="M17.77,34.048C7.971,34.048,0,42.019,0,51.817s7.971,17.77,17.77,17.77h5.844v-6.664H17.77
|
||||
c-6.124,0-11.106-4.982-11.106-11.106s4.982-11.106,11.106-11.106s11.132,4.982,11.132,11.106l0,0v16.365l0,0
|
||||
c0,6.084-4.954,11.039-11.023,11.103c-2.904-0.024-5.681-1.191-7.729-3.25l-4.712,4.712c3.266,3.283,7.691,5.151,12.321,5.201
|
||||
v0.003c0.04,0,0.08,0,0.119,0h0.125v-0.003c9.659-0.131,17.48-8.005,17.525-17.686l0.006-16.881
|
||||
C35.302,41.785,27.422,34.048,17.77,34.048z">
|
||||
</path>
|
||||
<path d="M124.083,34.137c-9.798,0-17.769,7.971-17.769,17.77s7.971,17.769,17.769,17.769h6.08v-6.663h-6.08
|
||||
c-6.124,0-11.106-4.983-11.106-11.106s4.982-11.106,11.106-11.106c5.799,0,10.572,4.468,11.062,10.143h-0.01v34.12h6.664V51.907
|
||||
l0,0C141.797,42.108,133.881,34.137,124.083,34.137z">
|
||||
</path>
|
||||
<polygon points="151.983,35.04 151.033,35.04 149.737,37.053 148.399,35.04 147.44,35.04 147.44,38.624 148.511,38.624
|
||||
148.511,36.88 149.461,38.288 149.979,38.288 150.912,36.836 150.929,38.624 152,38.624 ">
|
||||
</polygon>
|
||||
<polygon points="143.519,35.896 144.685,35.896 144.685,38.624 145.86,38.624 145.86,35.896 147.034,35.896 147.034,35.04
|
||||
143.519,35.04 ">
|
||||
</polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
8
src/frontend/src/icons/Groq/index.tsx
Normal file
8
src/frontend/src/icons/Groq/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgGroqLogo from "./GroqLogo";
|
||||
|
||||
export const GroqIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgGroqLogo ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-twilight";
|
||||
import {
|
||||
ReactNode,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import CodeTabsComponent from "../../components/codeTabsComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import {
|
||||
EXPORT_CODE_DIALOG,
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import { tweakType, uniqueTweakType } from "../../types/components";
|
||||
import { FlowType, NodeType } from "../../types/flow/index";
|
||||
import { buildTweaks, convertArrayToObj } from "../../utils/reactflowUtils";
|
||||
import {
|
||||
getCurlCode,
|
||||
getPythonApiCode,
|
||||
getPythonCode,
|
||||
getWidgetCode,
|
||||
tabsArray,
|
||||
} from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
const ApiModal = forwardRef(
|
||||
(
|
||||
{
|
||||
flow,
|
||||
children,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
children: ReactNode;
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { autoLogin } = useContext(AuthContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const tweak = useRef<tweakType>([]);
|
||||
const tweaksList = useRef<string[]>([]);
|
||||
const [getTweak, setTweak] = useState<tweakType>([]);
|
||||
const flowState = useFlowStore((state) => state.flowState);
|
||||
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
|
||||
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
|
||||
const pythonCode = getPythonCode(flow, tweak.current);
|
||||
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
|
||||
const tweaksCode = buildTweaks(flow);
|
||||
const codesArray = [
|
||||
curl_code,
|
||||
pythonApiCode,
|
||||
pythonCode,
|
||||
widgetCode,
|
||||
pythonCode,
|
||||
];
|
||||
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
|
||||
|
||||
function startState() {
|
||||
tweak.current = [];
|
||||
setTweak([]);
|
||||
tweaksList.current = [];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (flow["data"]!["nodes"].length == 0) {
|
||||
startState();
|
||||
} else {
|
||||
tweak.current = [];
|
||||
const t = buildTweaks(flow);
|
||||
tweak.current.push(t);
|
||||
}
|
||||
|
||||
filterNodes();
|
||||
|
||||
if (Object.keys(tweaksCode).length > 0) {
|
||||
setActiveTab("0");
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
} else {
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
}
|
||||
}, [flow["data"]!["nodes"], open]);
|
||||
|
||||
function filterNodes() {
|
||||
let arrNodesWithValues: string[] = [];
|
||||
|
||||
flow["data"]!["nodes"].forEach((node) => {
|
||||
if (!node["data"]["node"]["template"]) {
|
||||
return;
|
||||
}
|
||||
Object.keys(node["data"]["node"]["template"])
|
||||
.filter(
|
||||
(templateField) =>
|
||||
templateField.charAt(0) !== "_" &&
|
||||
node.data.node.template[templateField].show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
node.data.node.template[templateField].type
|
||||
)
|
||||
)
|
||||
.map((n, i) => {
|
||||
arrNodesWithValues.push(node["id"]);
|
||||
});
|
||||
});
|
||||
|
||||
tweaksList.current = arrNodesWithValues.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
function buildTweakObject(
|
||||
tw: string,
|
||||
changes: string | string[] | boolean | number | Object[] | Object,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
if (typeof changes === "string" && template.type === "float") {
|
||||
changes = parseFloat(changes);
|
||||
}
|
||||
if (typeof changes === "string" && template.type === "int") {
|
||||
changes = parseInt(changes);
|
||||
}
|
||||
if (template.list === true && Array.isArray(changes)) {
|
||||
changes = changes?.filter((x) => x !== "");
|
||||
}
|
||||
|
||||
if (template.type === "dict" && Array.isArray(changes)) {
|
||||
changes = convertArrayToObj(changes);
|
||||
}
|
||||
|
||||
if (template.type === "NestedDict") {
|
||||
changes = JSON.stringify(changes);
|
||||
}
|
||||
|
||||
const existingTweak = tweak.current.find((element) =>
|
||||
element.hasOwnProperty(tw)
|
||||
);
|
||||
|
||||
if (existingTweak) {
|
||||
existingTweak[tw][template["name"]!] = changes as string;
|
||||
|
||||
if (existingTweak[tw][template["name"]!] == template.value) {
|
||||
tweak.current.forEach((element) => {
|
||||
if (element[tw] && Object.keys(element[tw])?.length === 0) {
|
||||
tweak.current = tweak.current.filter((obj) => {
|
||||
const prop = obj[Object.keys(obj)[0]].prop;
|
||||
return prop !== undefined && prop !== null && prop !== "";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newTweak = {
|
||||
[tw]: {
|
||||
[template["name"]!]: changes,
|
||||
},
|
||||
} as uniqueTweakType;
|
||||
tweak.current.push(newTweak);
|
||||
}
|
||||
|
||||
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
|
||||
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
|
||||
const pythonCode = getPythonCode(flow, tweak.current);
|
||||
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
|
||||
|
||||
tabs![0].code = curl_code;
|
||||
tabs![1].code = pythonApiCode;
|
||||
tabs![2].code = pythonCode;
|
||||
tabs![3].code = widgetCode;
|
||||
|
||||
setTweak(tweak.current);
|
||||
}
|
||||
|
||||
function buildContent(value: string) {
|
||||
const htmlContent = (
|
||||
<div className="w-[200px]">
|
||||
<span>{value != null && value != "" ? value : "None"}</span>
|
||||
</div>
|
||||
);
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
function getValue(
|
||||
value: string,
|
||||
node: NodeType,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
let returnValue = value ?? "";
|
||||
|
||||
if (getTweak.length > 0) {
|
||||
for (const obj of getTweak) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
if (key == node["id"]) {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (key == template["name"]) {
|
||||
returnValue = value[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return value ?? "";
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
|
||||
<span className="pr-2">API</span>
|
||||
<IconComponent
|
||||
name="Code2"
|
||||
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<CodeTabsComponent
|
||||
flow={flow}
|
||||
tabs={tabs!}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tweaks={{
|
||||
tweak,
|
||||
tweaksList,
|
||||
buildContent,
|
||||
buildTweakObject,
|
||||
getValue,
|
||||
}}
|
||||
/>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ApiModal;
|
||||
140
src/frontend/src/modals/BundleModal/component/index.tsx
Normal file
140
src/frontend/src/modals/BundleModal/component/index.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import InputComponent from "../../../components/inputComponent";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "../../../components/ui/form";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||
|
||||
type FolderFormsProps = {
|
||||
control: any;
|
||||
setValue: any;
|
||||
folderToEdit: any;
|
||||
};
|
||||
|
||||
export const FolderForms = ({
|
||||
control,
|
||||
setValue,
|
||||
folderToEdit,
|
||||
}: FolderFormsProps): JSX.Element => {
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
|
||||
const [selectedFlows, setSelectedFlows] = useState<string[]>([]);
|
||||
|
||||
const componentsList = flows
|
||||
.filter((flow) => flow.is_component && flow.folder_id !== null)
|
||||
.map((flow) => ({ id: flow.id, name: flow.name }));
|
||||
|
||||
const flowsList = flows
|
||||
.filter((flow) => !flow.is_component && flow.folder_id !== null)
|
||||
.map((flow) => ({ id: flow.id, name: flow.name }));
|
||||
|
||||
useEffect(() => {
|
||||
setValue("components", selectedComponents);
|
||||
setValue("flows", selectedFlows);
|
||||
}, [selectedComponents, selectedFlows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (folderToEdit) {
|
||||
setValue("name", folderToEdit.name);
|
||||
setValue("description", folderToEdit.description);
|
||||
console.log(folderToEdit);
|
||||
|
||||
// setSelectedComponents(folderToEdit.components);
|
||||
// setSelectedFlows(folderToEdit.flows);
|
||||
return;
|
||||
}
|
||||
setValue("name", "");
|
||||
setValue("description", "");
|
||||
setSelectedComponents([]);
|
||||
setSelectedFlows([]);
|
||||
}, [folderToEdit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full flex-col gap-4 align-middle">
|
||||
<Label>Folder Name</Label>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="Insert a name for the folder..."
|
||||
></Input>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Label>Description (optional) </Label>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<Textarea
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="Insert a description for the folder..."
|
||||
></Textarea>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Label>Add Flows</Label>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="flows"
|
||||
render={() => (
|
||||
<FormControl>
|
||||
<InputComponent
|
||||
isObjectOption
|
||||
password={false}
|
||||
objectOptions={flowsList}
|
||||
placeholder="Choose a flow to add..."
|
||||
id="input-flow"
|
||||
setSelectedOptions={(value: any) => setSelectedFlows(value)}
|
||||
selectedOptions={selectedFlows}
|
||||
></InputComponent>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Label>Add Components</Label>
|
||||
<FormField
|
||||
control={control}
|
||||
name="components"
|
||||
render={() => (
|
||||
<FormControl>
|
||||
<InputComponent
|
||||
isObjectOption
|
||||
password={false}
|
||||
objectOptions={componentsList}
|
||||
placeholder="Choose a component to add..."
|
||||
id="input-component"
|
||||
setSelectedOptions={(value) => setSelectedComponents(value)}
|
||||
selectedOptions={selectedComponents}
|
||||
></InputComponent>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderForms;
|
||||
10
src/frontend/src/modals/BundleModal/entities/index.ts
Normal file
10
src/frontend/src/modals/BundleModal/entities/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const FolderFormsSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name must be at least 1 characters.",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
components: z.array(z.string()),
|
||||
flows: z.array(z.string()),
|
||||
});
|
||||
53
src/frontend/src/modals/BundleModal/hooks/submit-folder.tsx
Normal file
53
src/frontend/src/modals/BundleModal/hooks/submit-folder.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { addFolder, updateFolder } from "../../../pages/MainPage/services";
|
||||
import useAlertStore from "../../../stores/alertStore";
|
||||
import { useFolderStore } from "../../../stores/foldersStore";
|
||||
|
||||
const useFolderSubmit = (setOpen, folderToEdit) => {
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
|
||||
|
||||
const onSubmit = (data) => {
|
||||
if (folderToEdit) {
|
||||
updateFolder(data, folderToEdit?.id!).then(
|
||||
() => {
|
||||
setSuccessData({
|
||||
title: "Folder updated successfully.",
|
||||
});
|
||||
getFoldersApi(true);
|
||||
setOpen(false);
|
||||
},
|
||||
(reason) => {
|
||||
if (reason) {
|
||||
setErrorData({
|
||||
title: `Error updating folder.`,
|
||||
});
|
||||
console.error(reason);
|
||||
} else {
|
||||
getFoldersApi(true);
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
addFolder(data).then(
|
||||
() => {
|
||||
setSuccessData({
|
||||
title: "Folder created successfully.",
|
||||
});
|
||||
getFoldersApi(true);
|
||||
setOpen(false);
|
||||
},
|
||||
() => {
|
||||
setErrorData({
|
||||
title: `Error creating folder.`,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return { onSubmit, open, setOpen };
|
||||
};
|
||||
|
||||
export default useFolderSubmit;
|
||||
79
src/frontend/src/modals/BundleModal/index.tsx
Normal file
79
src/frontend/src/modals/BundleModal/index.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import ForwardedIconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Form } from "../../components/ui/form";
|
||||
import { useFolderStore } from "../../stores/foldersStore";
|
||||
import BaseModal from "../baseModal";
|
||||
import FolderForms from "./component";
|
||||
import { FolderFormsSchema } from "./entities";
|
||||
import useFolderSubmit from "./hooks/submit-folder";
|
||||
|
||||
type FoldersModalProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export default function FoldersModal({
|
||||
open,
|
||||
setOpen,
|
||||
}: FoldersModalProps): JSX.Element {
|
||||
const form = useForm<z.infer<typeof FolderFormsSchema>>({
|
||||
resolver: zodResolver(FolderFormsSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
components: [],
|
||||
flows: [],
|
||||
},
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const folderToEdit = useFolderStore((state) => state.folderToEdit);
|
||||
const { onSubmit: onSubmitFolder } = useFolderSubmit(setOpen, folderToEdit);
|
||||
|
||||
const onSubmit = (data) => {
|
||||
onSubmitFolder(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Header
|
||||
description={`${folderToEdit ? "Edit a folder" : "Add a new folder"}`}
|
||||
>
|
||||
<span className="pr-2" data-testid="modal-title">
|
||||
{folderToEdit ? "Edit" : "New"} Folder
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FolderForms
|
||||
folderToEdit={folderToEdit}
|
||||
control={form.control}
|
||||
setValue={form.setValue}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="float-right mt-6"
|
||||
type="submit"
|
||||
disabled={!form.formState.isValid}
|
||||
>
|
||||
{folderToEdit ? "Edit" : "Save"} Folder
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import JsonView from "react18-json-view";
|
||||
import { useDarkStore } from "../../../../../../stores/darkStore";
|
||||
import { DictComponentType } from "../../../../../../types/components";
|
||||
|
||||
export default function IoJsonInput({
|
||||
value = [],
|
||||
onChange,
|
||||
left,
|
||||
output,
|
||||
}: DictComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (value) onChange(value);
|
||||
}, [value]);
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
ref.current = value;
|
||||
|
||||
const getClassNames = () => {
|
||||
if (!isDark && !left) return "json-view-playground-white";
|
||||
if (!isDark && left) return "json-view-playground-white-left";
|
||||
if (isDark && left) return "json-view-playground-dark-left";
|
||||
if (isDark && !left) return "json-view-playground-dark";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<JsonView
|
||||
className={getClassNames()}
|
||||
theme="vscode"
|
||||
dark={isDark}
|
||||
editable={!output}
|
||||
enableClipboard
|
||||
onEdit={(edit) => {
|
||||
ref.current = edit["src"];
|
||||
}}
|
||||
onChange={(edit) => {
|
||||
ref.current = edit["src"];
|
||||
}}
|
||||
src={ref.current}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import _ from "lodash";
|
||||
import { useRef } from "react";
|
||||
import IconComponent from "../../../../../../components/genericIconComponent";
|
||||
import { Input } from "../../../../../../components/ui/input";
|
||||
import { classNames } from "../../../../../../utils/utils";
|
||||
|
||||
export type IOKeyPairInputProps = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
duplicateKey: boolean;
|
||||
isList: boolean;
|
||||
isInputField?: boolean;
|
||||
};
|
||||
|
||||
const IOKeyPairInput = ({
|
||||
value,
|
||||
onChange,
|
||||
duplicateKey,
|
||||
isList = true,
|
||||
isInputField,
|
||||
}: IOKeyPairInputProps) => {
|
||||
const checkValueType = (value) => {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
};
|
||||
|
||||
const ref = useRef<any>([]);
|
||||
ref.current =
|
||||
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
|
||||
|
||||
const handleChangeKey = (event, idx) => {
|
||||
const oldKey = Object.keys(ref.current[idx])[0];
|
||||
const updatedObj = { [event.target.value]: ref.current[idx][oldKey] };
|
||||
ref.current[idx] = updatedObj;
|
||||
onChange(ref.current);
|
||||
};
|
||||
|
||||
const handleChangeValue = (newValue, idx) => {
|
||||
const key = Object.keys(ref.current[idx])[0];
|
||||
ref.current[idx][key] = newValue;
|
||||
onChange(ref.current);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("flex h-full flex-col gap-3")}>
|
||||
{ref.current?.map((obj, index) => {
|
||||
return Object.keys(obj).map((key, idx) => {
|
||||
return (
|
||||
<div key={idx} className="flex w-full gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={key.trim()}
|
||||
className={classNames(duplicateKey ? "input-invalid" : "")}
|
||||
placeholder="Type key..."
|
||||
onChange={(event) => handleChangeKey(event, index)}
|
||||
disabled={!isInputField}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
value={obj[key]}
|
||||
placeholder="Type a value..."
|
||||
onChange={(event) =>
|
||||
handleChangeValue(event.target.value, index)
|
||||
}
|
||||
disabled={!isInputField}
|
||||
/>
|
||||
|
||||
{isList && isInputField && index === ref.current.length - 1 ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
let newInputList = _.cloneDeep(ref.current);
|
||||
newInputList.push({ "": "" });
|
||||
onChange(newInputList);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
className={"h-4 w-4 hover:text-accent-foreground"}
|
||||
/>
|
||||
</button>
|
||||
) : isList && isInputField ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
let newInputList = _.cloneDeep(ref.current);
|
||||
newInputList.splice(index, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
className="h-4 w-4 hover:text-status-red"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IOKeyPairInput;
|
||||
|
|
@ -1,9 +1,30 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useState } from "react";
|
||||
import ImageViewer from "../../../../components/ImageViewer";
|
||||
import CsvOutputComponent from "../../../../components/csvOutputComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import PdfViewer from "../../../../components/pdfViewer";
|
||||
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../../components/ui/select";
|
||||
import { Textarea } from "../../../../components/ui/textarea";
|
||||
import { PDFViewConstant } from "../../../../constants/constants";
|
||||
import { InputOutput } from "../../../../constants/enums";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import { IOFieldViewProps } from "../../../../types/components";
|
||||
import {
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import IOFileInput from "./components/FileInput";
|
||||
import IoJsonInput from "./components/JSONInput";
|
||||
import IOKeyPairInput from "./components/keyPairInput";
|
||||
|
||||
export default function IOFieldView({
|
||||
type,
|
||||
|
|
@ -15,6 +36,21 @@ export default function IOFieldView({
|
|||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node.id === fieldId);
|
||||
const flowPoolNode = (flowPool[node!.id] ?? [])[
|
||||
(flowPool[node!.id]?.length ?? 1) - 1
|
||||
];
|
||||
const handleChangeSelect = (e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
if (newNode.data.node.template.separator) {
|
||||
newNode.data.node.template.separator.value = e;
|
||||
setNode(newNode.id, newNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
function handleOutputType() {
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (type) {
|
||||
|
|
@ -53,6 +89,57 @@ export default function IOFieldView({
|
|||
/>
|
||||
);
|
||||
|
||||
case "KeyPairInput":
|
||||
return (
|
||||
<IOKeyPairInput
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
const valueToNumbers = convertValuesToNumbers(e);
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
}}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
isList={node.data.node!.template["input_value"]?.list ?? false}
|
||||
isInputField
|
||||
/>
|
||||
);
|
||||
|
||||
case "JsonInput":
|
||||
return (
|
||||
<IoJsonInput
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
left={left}
|
||||
/>
|
||||
);
|
||||
|
||||
case "StringListInput":
|
||||
return (
|
||||
<>
|
||||
<InputListComponent
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Textarea
|
||||
|
|
@ -91,6 +178,119 @@ export default function IOFieldView({
|
|||
readOnly
|
||||
/>
|
||||
);
|
||||
case "PDFOutput":
|
||||
return left ? (
|
||||
<div>{PDFViewConstant}</div>
|
||||
) : (
|
||||
<PdfViewer pdf={flowPoolNode?.params ?? ""} />
|
||||
);
|
||||
case "CSVOutput":
|
||||
return left ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
Expand the ouptut to see the CSV
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-5">
|
||||
<span>CSV separator </span>
|
||||
<Select
|
||||
value={node.data.node.template.separator.value}
|
||||
onValueChange={(e) => handleChangeSelect(e)}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{node?.data?.node?.template?.separator?.options.map(
|
||||
(separator) => (
|
||||
<SelectItem key={separator} value={separator}>
|
||||
{separator}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CsvOutputComponent csvNode={node} flowPool={flowPoolNode} />
|
||||
</>
|
||||
);
|
||||
case "ImageOutput":
|
||||
return left ? (
|
||||
<div>Expand the view to see the image</div>
|
||||
) : (
|
||||
<ImageViewer
|
||||
image={
|
||||
(flowPool[node.id] ?? [])[
|
||||
(flowPool[node.id]?.length ?? 1) - 1
|
||||
]?.params ?? ""
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case "JsonOutput":
|
||||
return (
|
||||
<IoJsonInput
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
left={left}
|
||||
output
|
||||
/>
|
||||
);
|
||||
|
||||
case "KeyPairOutput":
|
||||
return (
|
||||
<IOKeyPairInput
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
const valueToNumbers = convertValuesToNumbers(e);
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
}}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
isList={node.data.node!.template["input_value"]?.list ?? false}
|
||||
/>
|
||||
);
|
||||
|
||||
case "StringListOutput":
|
||||
return (
|
||||
<>
|
||||
<InputListComponent
|
||||
value={node.data.node!.template["input_value"]?.value}
|
||||
onChange={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
playgroundDisabled
|
||||
disabled={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case "RecordsOutput":
|
||||
return (
|
||||
<div className={left ? "h-56" : "h-full"}>
|
||||
<RecordsOutputComponent
|
||||
flowPool={flowPoolNode}
|
||||
pagination={!left}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import remarkGfm from "remark-gfm";
|
|||
import remarkMath from "remark-math";
|
||||
import MaleTechnology from "../../../../../assets/male-technologist.png";
|
||||
import Robot from "../../../../../assets/robot.png";
|
||||
import SanitizedHTMLWrapper from "../../../../../components/SanitizedHTMLWrapper";
|
||||
import CodeTabsComponent from "../../../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import SanitizedHTMLWrapper from "../../../../../components/sanitizedHTMLWrapper";
|
||||
import useAlertStore from "../../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../../stores/flowStore";
|
||||
import { chatMessagePropsType } from "../../../../../types/components";
|
||||
|
|
@ -114,19 +114,19 @@ export default function ChatMessage({
|
|||
<div
|
||||
className={classNames(
|
||||
"form-modal-chat-position",
|
||||
chat.isSend ? "" : " "
|
||||
chat.isSend ? "" : " ",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3"
|
||||
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl",
|
||||
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon"
|
||||
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
|
|
@ -210,12 +210,12 @@ dark:prose-invert"
|
|||
|
||||
children[0] = (children[0] as string).replace(
|
||||
"`▍`",
|
||||
"▍"
|
||||
"▍",
|
||||
);
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(
|
||||
className || ""
|
||||
className || "",
|
||||
);
|
||||
|
||||
return !inline ? (
|
||||
|
|
@ -230,7 +230,7 @@ dark:prose-invert"
|
|||
language: (match && match[1]) || "",
|
||||
code: String(children).replace(
|
||||
/\n$/,
|
||||
""
|
||||
"",
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
|
@ -248,7 +248,7 @@ dark:prose-invert"
|
|||
{chatMessage}
|
||||
</Markdown>
|
||||
),
|
||||
[chat.message, chatMessage]
|
||||
[chat.message, chatMessage],
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -256,7 +256,7 @@ dark:prose-invert"
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="form-modal-chat-text-position min-w-96 flex-grow">
|
||||
{template ? (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -309,6 +309,7 @@ dark:prose-invert"
|
|||
</>
|
||||
) : (
|
||||
<span
|
||||
className="prose text-primary word-break-break-word dark:prose-invert"
|
||||
data-testid={
|
||||
"chat-message-" + chat.sender_name + "-" + chatMessage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import AccordionComponent from "../../components/AccordionComponent";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import AccordionComponent from "../../components/accordionComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
|
|
@ -34,25 +34,25 @@ export default function IOModal({
|
|||
}: IOModalPropsType): JSX.Element {
|
||||
const allNodes = useFlowStore((state) => state.nodes);
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput"
|
||||
(input) => input.type !== "ChatInput",
|
||||
);
|
||||
const chatInput = useFlowStore((state) => state.inputs).find(
|
||||
(input) => input.type === "ChatInput"
|
||||
(input) => input.type === "ChatInput",
|
||||
);
|
||||
const outputs = useFlowStore((state) => state.outputs).filter(
|
||||
(output) => output.type !== "ChatOutput"
|
||||
(output) => output.type !== "ChatOutput",
|
||||
);
|
||||
const chatOutput = useFlowStore((state) => state.outputs).find(
|
||||
(output) => output.type === "ChatOutput"
|
||||
(output) => output.type === "ChatOutput",
|
||||
);
|
||||
const nodes = useFlowStore((state) => state.nodes).filter(
|
||||
(node) =>
|
||||
inputs.some((input) => input.id === node.id) ||
|
||||
outputs.some((output) => output.id === node.id)
|
||||
outputs.some((output) => output.id === node.id),
|
||||
);
|
||||
const haveChat = chatInput || chatOutput;
|
||||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0,
|
||||
);
|
||||
|
||||
function startView() {
|
||||
|
|
@ -83,11 +83,11 @@ export default function IOModal({
|
|||
return updateVerticesOrder(currentFlow!.id, null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
updateVertices();
|
||||
}
|
||||
}, [open, currentFlow]);
|
||||
// useEffect(() => {
|
||||
// if (open) {
|
||||
// updateVertices();
|
||||
// }
|
||||
// }, [open, currentFlow]);
|
||||
|
||||
async function sendMessage({repeat=1,files}:{repeat:number,files?:string[]}): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
|
|
@ -124,7 +124,7 @@ export default function IOModal({
|
|||
|
||||
return (
|
||||
<BaseModal
|
||||
size={selectedTab === 0 ? "large-thin" : "large"}
|
||||
size={selectedTab === 0 ? "sm-thin" : "md-thin"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disable}
|
||||
|
|
@ -133,9 +133,9 @@ export default function IOModal({
|
|||
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
|
||||
<BaseModal.Header description={CHAT_FORM_DIALOG_SUBTITLE}>
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2">Interaction Panel</span>
|
||||
<span className="pr-2">Playground</span>
|
||||
<IconComponent
|
||||
name="prompts"
|
||||
name="BotMessageSquareIcon"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -147,7 +147,7 @@ export default function IOModal({
|
|||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300"
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
|
|
@ -180,11 +180,11 @@ export default function IOModal({
|
|||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id)
|
||||
inputs.some((input) => input.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id
|
||||
(input) => input.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -248,11 +248,11 @@ export default function IOModal({
|
|||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id)
|
||||
outputs.some((output) => output.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id
|
||||
(output) => output.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -315,7 +315,7 @@ export default function IOModal({
|
|||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col items-start gap-4 pt-4",
|
||||
!selectedViewField ? "hidden" : ""
|
||||
!selectedViewField ? "hidden" : "",
|
||||
)}
|
||||
>
|
||||
<div className="font-xl flex items-center justify-center gap-3 font-semibold">
|
||||
|
|
@ -334,7 +334,7 @@ export default function IOModal({
|
|||
</div>
|
||||
<div className="h-full w-full">
|
||||
{inputs.some(
|
||||
(input) => input.id === selectedViewField.id
|
||||
(input) => input.id === selectedViewField.id,
|
||||
) ? (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
|
|
@ -356,7 +356,7 @@ export default function IOModal({
|
|||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full",
|
||||
selectedViewField ? "hidden" : ""
|
||||
selectedViewField ? "hidden" : "",
|
||||
)}
|
||||
>
|
||||
{haveChat ? (
|
||||
|
|
@ -391,7 +391,7 @@ export default function IOModal({
|
|||
"h-4 w-4",
|
||||
isBuilding
|
||||
? "animate-spin"
|
||||
: "fill-current text-medium-indigo"
|
||||
: "fill-current text-medium-indigo",
|
||||
)}
|
||||
/>
|
||||
Run Flow
|
||||
|
|
|
|||
10
src/frontend/src/modals/apiModal/utils/build-content.tsx
Normal file
10
src/frontend/src/modals/apiModal/utils/build-content.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export function buildContent(value: string) {
|
||||
const htmlContent = (
|
||||
<div className="w-[200px]">
|
||||
<span>{value != null && value != "" ? value : "None"}</span>
|
||||
</div>
|
||||
);
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
export default buildContent;
|
||||
8
src/frontend/src/modals/apiModal/utils/build-tweaks.ts
Normal file
8
src/frontend/src/modals/apiModal/utils/build-tweaks.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
export function buildTweaks(flow: FlowType) {
|
||||
return flow.data!.nodes.reduce((acc, node) => {
|
||||
acc[node.data.id] = {};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
|
||||
|
||||
export const checkCanBuildTweakObject = (element, templateField) => {
|
||||
return (
|
||||
element.data.node.template[templateField] &&
|
||||
templateField.charAt(0) !== "_" &&
|
||||
element.data.node.template[templateField].show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
element.data.node.template[templateField].type,
|
||||
) &&
|
||||
templateField !== "code"
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue