Merge remote-tracking branch 'origin/dev' into chatImg

This commit is contained in:
ogabrielluiz 2024-06-06 09:34:13 -03:00
commit 8b1234f6ca
598 changed files with 26867 additions and 22399 deletions

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
@ -40,6 +41,7 @@
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
"debounce-promise": "^3.1.2",
"dompurify": "^3.0.5",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",
@ -2761,6 +2763,31 @@
}
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz",
"integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
@ -5670,6 +5697,11 @@
"node": ">=12"
}
},
"node_modules/debounce-promise": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
"integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View file

@ -21,6 +21,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
@ -35,6 +36,7 @@
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
"debounce-promise": "^3.1.2",
"dompurify": "^3.0.5",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",

View file

@ -15,7 +15,7 @@ dotenv.config({ path: path.resolve(__dirname, "../../.env") });
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
@ -52,18 +52,18 @@ export default defineConfig({
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"dom.events.asyncClipboard.readText": true,
"dom.events.testing.asyncClipboard": true,
},
},
},
},
// {
// name: "firefox",
// use: {
// ...devices["Desktop Firefox"],
// launchOptions: {
// firefoxUserPrefs: {
// "dom.events.asyncClipboard.readText": true,
// "dom.events.testing.asyncClipboard": true,
// },
// },
// },
// },
],
webServer: [
{

View file

@ -1,3 +1,4 @@
import axios from "axios";
import { useContext, useEffect, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
@ -15,6 +16,8 @@ import {
} from "./constants/constants";
import { AuthContext } from "./contexts/authContext";
import { autoLogin, getGlobalVariables, getHealth } from "./controllers/API";
import { setupAxiosDefaults } from "./controllers/API/utils";
import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path";
import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
@ -24,6 +27,8 @@ import { useGlobalVariablesStore } from "./stores/globalVariablesStore/globalVar
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
useTrackLastVisitedPath();
const removeFromTempNotificationList = useAlertStore(
(state) => state.removeFromTempNotificationList,
);
@ -104,7 +109,6 @@ export default function App() {
const fetchAllData = async () => {
setTimeout(async () => {
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
getFoldersApi();
}, 1000);
};
@ -112,6 +116,8 @@ export default function App() {
return new Promise<void>(async (resolve, reject) => {
if (isAuthenticated) {
try {
await setupAxiosDefaults();
await getFoldersApi();
await getTypes();
await refreshFlows();
const res = await getGlobalVariables();
@ -208,7 +214,7 @@ export default function App() {
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" && (
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.title}
@ -216,14 +222,7 @@ export default function App() {
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "notice" ? (
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}

View file

@ -16,13 +16,13 @@ export default function AlertDropdown({
}: AlertDropdownType): JSX.Element {
const notificationList = useAlertStore((state) => state.notificationList);
const clearNotificationList = useAlertStore(
(state) => state.clearNotificationList
(state) => state.clearNotificationList,
);
const removeFromNotificationList = useAlertStore(
(state) => state.removeFromNotificationList
(state) => state.removeFromNotificationList,
);
const setNotificationCenter = useAlertStore(
(state) => state.setNotificationCenter
(state) => state.setNotificationCenter,
);
const [open, setOpen] = useState(false);
@ -36,7 +36,7 @@ export default function AlertDropdown({
}}
>
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="nocopy nopan nodelete nodrag noundo flex h-[500px] w-[500px] flex-col">
<PopoverContent className="nocopy nowheel nopan nodelete nodrag noundo flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">

View file

@ -40,7 +40,7 @@ export default function ErrorAlert({
removeAlert(id);
}, 500);
}}
className="error-build-message nocopy nopan nodelete nodrag noundo"
className="error-build-message nocopy nowheel nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -36,7 +36,7 @@ export default function NoticeAlert({
setShow(false);
removeAlert(id);
}}
className="nocopy nopan nodelete nodrag noundo mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
className="nocopy nowheel nopan nodelete nodrag noundo mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -34,7 +34,7 @@ export default function SuccessAlert({
setShow(false);
removeAlert(id);
}}
className="success-alert nocopy nopan nodelete nodrag noundo"
className="success-alert nocopy nowheel nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -6,10 +6,13 @@ import {
AccordionTrigger,
} from "../../components/ui/accordion";
import { AccordionComponentType } from "../../types/components";
import { cn } from "../../utils/utils";
import ShadTooltip from "../shadTooltipComponent";
export default function AccordionComponent({
trigger,
children,
disabled,
open = [],
keyValue,
sideBar,
@ -29,7 +32,9 @@ export default function AccordionComponent({
}
function handleClick(): void {
value === "" ? setValue(keyValue!) : setValue("");
if (!disabled) {
value === "" ? setValue(keyValue!) : setValue("");
}
}
return (
@ -38,16 +43,18 @@ export default function AccordionComponent({
type="single"
className="w-full"
value={value}
onValueChange={setValue}
onValueChange={!disabled ? setValue : () => {}}
>
<AccordionItem value={keyValue!} className="border-b">
<AccordionTrigger
onClick={() => {
handleClick();
}}
className={
sideBar ? "w-full bg-muted px-[0.75rem] py-[0.5rem]" : "ml-3"
}
disabled={disabled}
className={cn(
sideBar ? "w-full bg-muted px-[0.75rem] py-[0.5rem]" : "ml-3",
disabled ? "cursor-not-allowed" : "cursor-pointer",
)}
>
{trigger}
</AccordionTrigger>

View file

@ -7,7 +7,6 @@ import { useTypesStore } from "../../stores/typesStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import ForwardedIconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
@ -65,12 +64,17 @@ export default function AddNewVariableButton({ children }): JSX.Element {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error creating variable",
list: [responseError.response.data.detail ?? "Unknown error"],
list: [responseError?.response?.data?.detail ?? "Unknown error"],
});
});
}
return (
<BaseModal open={open} setOpen={setOpen} size="x-small">
<BaseModal
open={open}
setOpen={setOpen}
size="x-small"
onSubmit={handleSaveVariable}
>
<BaseModal.Header
description={
"This variable will be encrypted and will be available for you to use in any of your projects."
@ -137,9 +141,9 @@ export default function AddNewVariableButton({ children }): JSX.Element {
></InputComponent>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button onClick={handleSaveVariable}>Save Variable</Button>
</BaseModal.Footer>
<BaseModal.Footer
submit={{ label: "Save Variable", dataTestId: "save-variable-btn" }}
/>
</BaseModal>
);
}

View file

@ -1,3 +1,3 @@
export default function sortByName(stringList: string[]): string[] {
return stringList.sort((a, b) => a.localeCompare(b));
}
}

View file

@ -1,7 +1,6 @@
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 }) {

View file

@ -1,3 +1,3 @@
export function convertTestName(name: string): string {
return name.replace(/ /g, "-").toLowerCase();
return name.replace(/ /g, "-").toLowerCase();
}

View file

@ -841,9 +841,7 @@ export default function CodeTabsComponent({
node.data.node!.template[
templateField
].value?.toString() === "{}"
? {
// yourkey: "value",
}
? {}
: node.data.node!
.template[
templateField

View file

@ -12,6 +12,9 @@ export default function DictComponent({
editNode = false,
id = "",
}: DictComponentType): JSX.Element {
// Create a reference to the value
const ref = useRef(value);
useEffect(() => {
if (disabled) {
onChange({});
@ -19,15 +22,14 @@ export default function DictComponent({
}, [disabled]);
useEffect(() => {
if (value) onChange(value);
// Update the reference value
ref.current = value;
}, [value]);
const ref = useRef(value);
return (
<div
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
"flex flex-col gap-3",
)}
>
{

View file

@ -9,11 +9,14 @@ export const EditFlowSettings: React.FC<InputProps> = ({
name,
invalidNameList,
description,
endpointName,
maxLength = 50,
setName,
setDescription,
setEndpointName,
}: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const [isEndpointNameValid, setIsEndpointNameValid] = useState(true);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
@ -29,6 +32,18 @@ export const EditFlowSettings: React.FC<InputProps> = ({
setDescription!(event.target.value);
};
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
// Validate the endpoint name
// use this regex r'^[a-zA-Z0-9_-]+$'
const isValid =
(/^[a-zA-Z0-9_-]+$/.test(event.target.value) &&
event.target.value.length <= maxLength) ||
// empty is also valid
event.target.value.length === 0;
setIsEndpointNameValid(isValid);
setEndpointName!(event.target.value);
};
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
const handleFocus = (event) => event.target.select();
@ -84,13 +99,39 @@ export const EditFlowSettings: React.FC<InputProps> = ({
<span
className={cn(
"font-normal text-muted-foreground word-break-break-word",
description === "" ? "font-light italic" : ""
description === "" ? "font-light italic" : "",
)}
>
{description === "" ? "No description" : description}
</span>
)}
</Label>
{setEndpointName && (
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium">Endpoint Name</span>
{!isEndpointNameValid && (
<span className="edit-flow-span">
Invalid endpoint name. Use only letters, numbers, hyphens, and
underscores ({maxLength} characters max).
</span>
)}
</div>
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleEndpointNameChange}
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name to run the endpoint"
maxLength={maxLength}
id="endpoint_name"
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/>
</Label>
)}
</>
);
};

View file

@ -1,7 +1,6 @@
import BaseModal from "../../modals/baseModal";
import { fetchErrorComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function FetchErrorComponent({
message,
@ -12,7 +11,14 @@ export default function FetchErrorComponent({
}: fetchErrorComponentType) {
return (
<>
<BaseModal size="small-h-full" open={openModal} type="modal">
<BaseModal
size="small-h-full"
open={openModal}
type="modal"
onSubmit={() => {
setRetry();
}}
>
<BaseModal.Content>
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent
@ -27,24 +33,9 @@ export default function FetchErrorComponent({
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="m-auto">
<Button
disabled={isLoadingHealth}
onClick={() => {
setRetry();
}}
>
{isLoadingHealth ? (
<div>
<IconComponent name={"Loader2"} className={"animate-spin"} />
</div>
) : (
"Retry"
)}
</Button>
</div>
</BaseModal.Footer>
<BaseModal.Footer
submit={{ label: "Retry", loading: isLoadingHealth }}
/>
</BaseModal>
</>
);

View file

@ -8,7 +8,6 @@ import {
} from "../../../ui/dropdown-menu";
import { useNavigate } from "react-router-dom";
import { Node } from "reactflow";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
@ -22,11 +21,7 @@ import IconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({
removeFunction,
}: {
removeFunction: (nodes: Node[]) => void;
}): JSX.Element => {
export const MenuBar = ({}: {}): JSX.Element => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -36,26 +31,15 @@ export const MenuBar = ({
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();
const isBuilding = useFlowStore((state) => state.isBuilding);
function handleAddFlow(duplicate?: boolean) {
function handleAddFlow() {
try {
if (duplicate) {
if (!currentFlow) {
throw new Error("No flow to duplicate");
}
addFlow(true, currentFlow).then((id) => {
setSuccessData({ title: "Flow duplicated successfully" });
navigate("/flow/" + id);
});
} else {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
@ -72,14 +56,6 @@ export const MenuBar = ({
return currentFlow ? (
<div className="round-button-div">
<button
onClick={() => {
removeFunction(nodes);
navigate("/");
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -103,15 +79,6 @@ export const MenuBar = ({
<IconComponent name="Plus" className="header-menu-options" />
New
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleAddFlow(true);
}}
className="cursor-pointer"
>
<IconComponent name="Copy" className="header-menu-options" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {

View file

@ -3,14 +3,17 @@ import { FaDiscord, FaGithub } from "react-icons/fa";
import { RiTwitterXFill } from "react-icons/ri";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import {
LOCATIONS_TO_RETURN,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { Node } from "reactflow";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useLocationStore } from "../../stores/locationStore";
import { useStoreStore } from "../../stores/storeStore";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
@ -29,6 +32,7 @@ import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element {
const notificationCenter = useAlertStore((state) => state.notificationCenter);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const navigate = useNavigate();
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
@ -40,20 +44,56 @@ export default function Header(): JSX.Element {
const setDark = useDarkStore((state) => state.setDark);
const stars = useDarkStore((state) => state.stars);
async function checkForChanges(nodes: Node[]): Promise<void> {
const routeHistory = useLocationStore((state) => state.routeHistory);
async function checkForChanges(): Promise<void> {
if (nodes.length === 0) {
await removeFlow(id!);
}
}
const redirectToLastLocation = () => {
const lastFlowVisitedIndex = routeHistory
.reverse()
.findIndex(
(path) => path.includes("/flow/") && path !== location.pathname,
);
const lastFlowVisited = routeHistory[lastFlowVisitedIndex];
lastFlowVisited && !location.pathname.includes("/flow")
? navigate(lastFlowVisited)
: navigate("/all");
};
const visitedFlowPathBefore = () => {
const lastThreeVisitedPaths = routeHistory.slice(-3);
return lastThreeVisitedPaths.some((path) => path.includes("/flow/"));
};
const showArrowReturnIcon =
LOCATIONS_TO_RETURN.some((path) => location.pathname.includes(path)) &&
visitedFlowPathBefore();
return (
<div className="header-arrangement">
<div className="header-start-display lg:w-[30%]">
<Link to="/" onClick={() => checkForChanges(nodes)}>
<Link to="/all" className="cursor-pointer" onClick={checkForChanges}>
<span className="ml-4 text-2xl">⛓️</span>
</Link>
<MenuBar removeFunction={checkForChanges} />
{showArrowReturnIcon && (
<button
onClick={() => {
checkForChanges();
redirectToLastLocation();
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
)}
<MenuBar />
</div>
<div className="round-button-div">
<Link to="/">
<Button
@ -65,9 +105,7 @@ export default function Header(): JSX.Element {
: "secondary"
}
size="sm"
onClick={() => {
checkForChanges(nodes);
}}
onClick={checkForChanges}
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
@ -80,9 +118,7 @@ export default function Header(): JSX.Element {
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
onClick={() => {
checkForChanges(nodes);
}}
onClick={checkForChanges}
data-testid="button-store"
>
<IconComponent name="Store" className="h-4 w-4" />
@ -145,7 +181,7 @@ export default function Header(): JSX.Element {
/>
</div>
</AlertDropdown>
{!autoLogin && (
{autoLogin && (
<button
onClick={() => {
navigate("/account/api-keys");

View file

@ -108,7 +108,7 @@ const CustomInputPopover = ({
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"

View file

@ -80,7 +80,7 @@ const CustomInputPopoverObject = ({
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"

View file

@ -33,6 +33,8 @@ export default function InputComponent({
optionButton,
objectOptions,
isObjectOption = false,
name,
onChangeFolderName,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
@ -53,6 +55,7 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
name={name}
id={"form-" + id}
ref={refInput}
onBlur={onInputLostFocus}
@ -72,6 +75,9 @@ export default function InputComponent({
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
if (onChangeFolderName) {
return onChangeFolderName(e);
}
onChange && onChange(e.target.value);
}}
onCopy={(e) => {

View file

@ -66,10 +66,8 @@ export default function InputFileComponent({
uploadFile(file, currentFlowId)
.then((res) => res.data)
.then((data) => {
console.log(CONSOLE_SUCCESS_MSG);
// Get the file name from the response
const { file_path } = data;
console.log("File name:", file_path);
// sets the value that goes to the backend
onFileChange(file_path);

View file

@ -31,7 +31,7 @@ export default function InputListComponent({
<div
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
"flex flex-col gap-3",
)}
>
{value.map((singleValue, idx) => {
@ -55,10 +55,11 @@ export default function InputListComponent({
/>
{idx === value.length - 1 ? (
<button
onClick={() => {
onClick={(e) => {
let newInputList = _.cloneDeep(value);
newInputList.push("");
onChange(newInputList);
e.preventDefault();
}}
data-testid={
`input-list-plus-btn${
@ -79,10 +80,11 @@ export default function InputListComponent({
editNode ? "-edit" : ""
}_${componentName}-` + idx
}
onClick={() => {
onClick={(e) => {
let newInputList = _.cloneDeep(value);
newInputList.splice(idx, 1);
onChange(newInputList);
e.preventDefault();
}}
disabled={disabled || playgroundDisabled}
>

View file

@ -2,8 +2,8 @@ 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";
import TableComponent from "../tableComponent";
function RecordsOutputComponent({
flowPool,

View file

@ -13,7 +13,6 @@ export default function ShadTooltip({
return (
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent
className={cn(styleClasses, "max-w-96")}
side={side}

View file

@ -1,6 +1,7 @@
import { Link } from "react-router-dom";
import { cn } from "../../../../utils/utils";
import { buttonVariants } from "../../../ui/button";
import ForwardedIconComponent from "../../../genericIconComponent";
type SideBarButtonsComponentProps = {
items: {
@ -13,10 +14,10 @@ type SideBarButtonsComponentProps = {
};
const SideBarButtonsComponent = ({
items,
handleOpenNewFolderModal,
pathname,
}: SideBarButtonsComponentProps) => {
return (
<>
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
{items.map((item) => (
<Link to={item.href!}>
<div
@ -24,15 +25,20 @@ const SideBarButtonsComponent = ({
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",
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"flex w-full shrink-0 justify-start gap-4",
)}
onClick={handleOpenNewFolderModal}
>
{item.title}
{item.icon}
<span className="block max-w-full truncate opacity-100">
{item.title}
</span>
</div>
</Link>
))}
</>
</div>
);
};
export default SideBarButtonsComponent;

View file

@ -1,16 +1,18 @@
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
import { addFolder, updateFolder } from "../../../../pages/MainPage/services";
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { handleKeyDown } from "../../../../utils/reactflowUtils";
import { cn } from "../../../../utils/utils";
import DropdownButton from "../../../dropdownButtonComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../genericIconComponent";
import { Button, buttonVariants } from "../../../ui/button";
import { Input } from "../../../ui/input";
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[];
@ -18,22 +20,27 @@ type SideBarFoldersButtonsComponentProps = {
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
handleAddFolder?: () => void;
};
const SideBarFoldersButtonsComponent = ({
folders,
pathname,
handleAddFolder,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
}: SideBarFoldersButtonsComponentProps) => {
const refInput = useRef<HTMLInputElement>(null);
const setFolders = useFolderStore((state) => state.setFolders);
const folders = useFolderStore((state) => state.folders);
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })),
);
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 getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const folderIdDragging = useFolderStore((state) => state.folderIdDragging);
const checkPathName = (itemId: string) => {
if (urlWithoutPath && itemId === myCollectionId) {
@ -62,106 +69,240 @@ const SideBarFoldersButtonsComponent = ({
handleDownloadFolderFn(id);
};
function addNewFolder() {
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
},
);
}
function handleEditFolderName(e, name): void {
const {
target: { value },
} = e;
setFoldersNames((old) => ({
...old,
[name]: value,
}));
}
useEffect(() => {
folders.map((obj) => ({ name: obj.name, edit: false }));
}, [folders]);
return (
<>
<div className="flex shrink-0 items-center justify-between">
<DropdownButton
firstButtonName="New Folder"
onFirstBtnClick={handleAddFolder!}
options={[]}
plusButton={true}
dropdownOptions={false}
/>
<div className="flex shrink-0 items-center justify-between gap-2">
<div className="flex-1 self-start text-lg font-semibold">Folders</div>
<Button
variant="primary"
onClick={handleUploadFlowsToFolder}
className="px-7"
size="icon"
className="px-2"
onClick={addNewFolder}
data-testid="add-folder-button"
>
<ForwardedIconComponent
name="Upload"
className="main-page-nav-button"
/>
Upload
<ForwardedIconComponent name="FolderPlus" className="w-4" />
</Button>
<Button
variant="primary"
size="icon"
className="px-2"
onClick={handleUploadFlowsToFolder}
data-testid="upload-folder-button"
>
<ForwardedIconComponent name="Upload" className="w-4" />
</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>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
)[0];
return (
<div
onDragOver={(e) => dragOver(e, item.id!)}
onDragEnter={(e) => dragEnter(e, item.id!)}
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",
folderIdDragging === item.id! ? "bg-border" : "",
)}
{index > 0 && (
onClick={() => handleChangeFolder!(item.id!)}
>
<div
onDoubleClick={(event) => {
if (item.name === "My Projects") {
return;
}
if (!foldersNames[item.name]) {
setFoldersNames({ [item.name]: item.name });
}
if (
editFolders.find((obj) => obj.name === item.name)?.name
) {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: true };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
return;
}
setEditFolderName((old) => [
...old,
{ name: item.name, edit: true },
]);
setFoldersNames((oldFolder) => ({
...oldFolder,
[item.name]: item.name,
}));
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
className="flex w-full items-center gap-4"
>
<IconComponent
name={"folder"}
className="w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
{editFolderName?.edit ? (
<div>
<Input
className="w-36"
onChange={(e) => {
handleEditFolderName(e, item.name);
}}
ref={refInput}
onKeyDown={(e) => {
if (e.key === "Escape") {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
}
if (e.key === "Enter") {
refInput.current?.blur();
}
handleKeyDown(e, e.key, "");
}}
autoFocus={true}
onBlur={async () => {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
if (foldersNames[item.name].trim() !== "") {
setFoldersNames((old) => ({
...old,
[item.name]: foldersNames[item.name],
}));
const body = {
...item,
name: foldersNames[item.name],
flows: item.flows?.length > 0 ? item.flows : [],
components:
item.components?.length > 0
? item.components
: [],
};
const updatedFolder = await updateFolder(
body,
item.id!,
);
const updateFolders = folders.filter(
(f) => f.name !== item.name,
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
} else {
setFoldersNames((old) => ({
...old,
[item.name]: item.name,
}));
}
}}
value={foldersNames[item.name]}
id={`input-folder-${item.name}`}
data-testid={`input-folder`}
/>
</div>
) : (
<span className="block w-full truncate opacity-100">
{item.name}
</span>
)}
{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>
)}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleEditFolder!(item);
handleDownloadFolder(item.id!);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
size="none"
variant="none"
>
<IconComponent
name={"pencil"}
name={"Download"}
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>
))}
);
})}
</>
</div>
</>

View file

@ -7,13 +7,16 @@ 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";
import { addVersionToDuplicates } from "../../../utils/reactflowUtils";
const useFileDrop = (folderId, folderChangeCallback) => {
const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
const setFolderIdDragging = useFolderStore(
(state) => state.setFolderIdDragging,
);
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) => {
@ -42,12 +45,14 @@ const useFileDrop = (folderId, folderChangeCallback) => {
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
};
const dragEnter = (
@ -55,10 +60,12 @@ const useFileDrop = (folderId, folderChangeCallback) => {
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
e.preventDefault();
};
@ -71,6 +78,7 @@ const useFileDrop = (folderId, folderChangeCallback) => {
e.preventDefault();
if (e.target === e.currentTarget) {
setFolderDragging(false);
setFolderIdDragging("");
}
};
@ -92,7 +100,6 @@ const useFileDrop = (folderId, folderChangeCallback) => {
e.preventDefault();
handleFileDrop(e);
setFolderDragging(false);
};
const uploadFromDragCard = (flowId, folderId) => {
@ -101,11 +108,15 @@ const useFileDrop = (folderId, folderChangeCallback) => {
if (!selectedFlow) {
throw new Error("Flow not found");
}
const updatedFlow = { ...selectedFlow, folder_id: folderId };
const newName = addVersionToDuplicates(updatedFlow, flows);
updatedFlow.name = newName;
setFolderDragging(false);
setFolderIdDragging("");
const updatedFlow: FlowType = {
...selectedFlow,
folder_id: folderId,
};
updateFlowInDatabase(updatedFlow).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
@ -115,11 +126,11 @@ const useFileDrop = (folderId, folderChangeCallback) => {
const uploadFormData = (data) => {
const formData = new FormData();
formData.append("file", data);
setFolderDragging(false);
setFolderIdDragging("");
uploadFlowsFromFolders(formData).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
refreshFlows();
});
};

View file

@ -5,6 +5,9 @@ import { cn } from "../../utils/utils";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import SideBarButtonsComponent from "./components/sideBarButtons";
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
import { addFolder } from "../../pages/MainPage/services";
import { useNavigate } from "react-router-dom";
import useFlowStore from "../../stores/flowStore";
type SidebarNavProps = {
items: {
@ -22,7 +25,6 @@ type SidebarNavProps = {
export default function SidebarNav({
className,
items,
handleOpenNewFolderModal,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
@ -39,21 +41,20 @@ export default function SidebarNav({
return (
<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}
/>
{items.length > 0 ? (
<SideBarButtonsComponent items={items} pathname={pathname} />
) : (
!loadingFolders &&
folders?.length > 0 &&
isFolderPath && (
<SideBarFoldersButtonsComponent
folders={folders}
pathname={pathname}
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
/>
)
)}
</HorizontalScrollFadeComponent>
</nav>

View file

@ -5,7 +5,6 @@ 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({

View file

@ -0,0 +1,31 @@
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../shadTooltipComponent";
import { Toggle } from "../../../ui/toggle";
export default function ResetColumns({
resetGrid,
}: {
resetGrid: () => void;
}): JSX.Element {
return (
/*<div className="absolute left-2 bottom-1 cursor-pointer">
<div
className="flex h-10 items-center justify-center px-2 pl-3 rounded-md border border-ring/60 text-sm text-[#bccadc] ring-offset-background placeholder:text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => setShow(!show)}
>
<ForwardedIconComponent name="Settings"></ForwardedIconComponent>
<ForwardedIconComponent name={show ? "ChevronLeft" : "ChevronRight"} className="transition-all"></ForwardedIconComponent>
</div>
</div>*/
<div className={cn("absolute bottom-4 left-6")}>
<span
className="cursor-pointer underline"
onClick={() => {
resetGrid();
}}
>
Reset Columns
</span>
</div>
);
}

View file

@ -1,22 +1,27 @@
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 { ElementRef, forwardRef, useEffect, useRef } 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 { cn, toTitleCase } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Toggle } from "../ui/toggle";
import ShadTooltip from "../shadTooltipComponent";
import resetGrid from "./utils/reset-grid-columns";
import ResetColumns from "./components/ResetColumns";
interface TableComponentProps extends AgGridReactProps {
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
rowData: NonNullable<AgGridReactProps["rowData"]>;
alertTitle?: string;
alertDescription?: string;
editable?: boolean | string[];
}
const TableComponent = forwardRef<
@ -31,51 +36,66 @@ const TableComponent = forwardRef<
},
ref,
) => {
let colDef = props.columnDefs.map((col, index) => {
let newCol = {
...col,
headerName: toTitleCase(col.headerName),
};
if (index === props.columnDefs.length - 1) {
newCol = {
...newCol,
resizable: false,
};
}
if (props.onSelectionChanged && index === 0) {
newCol = {
...newCol,
checkboxSelection: true,
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
};
}
if (
(typeof props.editable === "boolean" && props.editable) ||
(Array.isArray(props.editable) &&
props.editable.includes(newCol.headerName ?? ""))
) {
newCol = {
...newCol,
editable: true,
};
}
return newCol;
});
const gridRef = useRef(null);
// @ts-ignore
const realRef = ref?.current ? ref : gridRef;
const dark = useDarkStore((state) => state.dark);
var currentRowHeight: number;
var minRowHeight = 25;
const initialColumnDefs = useRef(colDef);
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 makeLastColumnNonResizable = (columnDefs) => {
columnDefs.forEach((colDef, index) => {
colDef.resizable = index !== columnDefs.length - 1;
});
return columnDefs;
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onGridReady = (params) => {
// @ts-ignore
realRef.current = params;
const updatedColumnDefs = makeLastColumnNonResizable([...colDef]);
params.api.setColumnDefs(updatedColumnDefs);
initialColumnDefs.current = params.api.getColumnDefs();
if (props.onGridReady) props.onGridReady(params);
};
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onColumnMoved = (params) => {
const updatedColumnDefs = makeLastColumnNonResizable(
params.columnApi.getAllGridColumns().map((col) => col.getColDef()),
);
params.api.setColumnDefs(updatedColumnDefs);
if (props.onColumnMoved) props.onColumnMoved(params);
};
if (props.rowData.length === 0) {
return (
@ -91,26 +111,27 @@ const TableComponent = forwardRef<
</div>
);
}
return (
<div
className={cn(
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
"ag-theme-shadcn flex h-full flex-col",
"relative",
)} // 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}
columnDefs={colDef}
ref={realRef}
pagination={true}
onGridReady={onGridReady}
onColumnMoved={onColumnMoved}
/>
<ResetColumns resetGrid={() => resetGrid(realRef, initialColumnDefs)} />
</div>
);
},

View file

@ -0,0 +1,12 @@
export default function resetGrid(ref, initialColumnDefs) {
if (ref?.current && ref?.current.api) {
ref.current.api.resetColumnState();
if (initialColumnDefs.current) {
const resetColumns = ref.current.api.applyColumnState({
state: initialColumnDefs.current,
applyOrder: true,
});
return resetColumns;
}
}
}

View file

@ -4,6 +4,7 @@ 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";
import ShadTooltip from "../shadTooltipComponent";
const Accordion = AccordionPrimitive.Root;
@ -22,17 +23,33 @@ AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
>(({ className, children, disabled, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger asChild ref={ref} {...props}>
<AccordionPrimitive.Trigger
disabled={disabled}
asChild
ref={ref}
{...props}
>
<div
className={cn(
"flex flex-1 cursor-pointer items-center justify-between py-4 text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180",
className
className,
)}
>
{children}
<ChevronDownIcon className="h-4 w-4 font-bold text-primary transition-transform duration-200" />
<ShadTooltip
styleClasses="z-50"
content={disabled ? "Empty" : ""}
side="top"
>
<ChevronDownIcon
className={cn(
"h-4 w-4 font-bold transition-transform duration-200",
disabled ? "text-muted-foreground" : "text-primary",
)}
/>
</ShadTooltip>
</div>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@ -47,7 +64,7 @@ const AccordionContent = React.forwardRef<
ref={ref}
className={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
className
className,
)}
{...props}
>

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "../../utils/utils";
const alertVariants = cva(
@ -55,4 +55,4 @@ const AlertDescription = React.forwardRef<
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
export { Alert, AlertDescription, AlertTitle };

View file

@ -2,9 +2,10 @@ import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
variant: {
@ -19,6 +20,7 @@ const buttonVariants = cva(
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
none: "",
},
size: {
default: "h-10 py-2 px-4",
@ -26,19 +28,21 @@ const buttonVariants = cva(
xs: "py-0.5 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "py-1 px-1 rounded-md",
none: "",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
function toTitleCase(text: string) {
@ -49,21 +53,49 @@ function toTitleCase(text: string) {
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, children, ...props }, ref) => {
(
{
className,
variant,
size,
loading,
disabled,
asChild = false,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";
let newChildren = children;
if (typeof children === "string") {
newChildren = toTitleCase(children);
}
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
children={newChildren}
{...props}
/>
<>
<Comp
className={cn(buttonVariants({ variant, size, className }))}
disabled={loading || disabled}
ref={ref}
{...props}
>
{loading ? (
<span className="relative">
<span className="invisible">{newChildren}</span>
<span className="absolute inset-0 flex items-center justify-center">
<ForwardedIconComponent
name={"Loader2"}
className={"animate-spin"}
/>
</span>
</span>
) : (
newChildren
)}
</Comp>
</>
);
}
},
);
Button.displayName = "Button";

View file

@ -8,8 +8,8 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"flex flex-col justify-between rounded-lg border bg-muted text-card-foreground shadow-sm transition-all hover:shadow-lg",
className
"flex flex-col justify-between rounded-lg border bg-muted text-card-foreground shadow-sm transition-all",
className,
)}
{...props}
/>
@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-base font-semibold leading-tight tracking-tight",
className
className,
)}
{...props}
/>

View file

@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../utils/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };

View file

@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
className,
)}
{...props}
/>
@ -28,4 +28,26 @@ const TooltipContent = React.forwardRef<
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
const TooltipContentWithoutPortal = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className,
)}
{...props}
/>
));
TooltipContentWithoutPortal.displayName = TooltipPrimitive.Content.displayName;
export {
Tooltip,
TooltipContent,
TooltipContentWithoutPortal,
TooltipProvider,
TooltipTrigger,
};

View file

@ -590,6 +590,7 @@ export const CONTROL_PATCH_USER_STATE = {
password: "",
cnfPassword: "",
gradient: "",
apikey: "",
};
export const CONTROL_LOGIN_STATE = {
@ -606,84 +607,6 @@ export const CONTROL_NEW_USER = {
export const tabsCode = [];
export function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
];
}
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
language: "py",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[4],
},
];
}
export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
export const FETCH_ERROR_DESCRIPION =
"Check if everything is working properly and try again.";
@ -800,8 +723,21 @@ export const NATIVE_CATEGORIES = [
"agents",
];
export const AUTHORIZED_DUPLICATE_REQUESTS = [
"/health",
"/flows",
"/logout",
"/refresh",
"/login",
"/auto_login",
];
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";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];
export const MAX_BATCH_SIZE = 50;

View file

@ -6,6 +6,7 @@ import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import { checkDuplicateRequestAndStoreRequest } from "./helpers/check-duplicate-requests";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
@ -80,22 +81,12 @@ function ApiInterceptor() {
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
const lastUrl = localStorage.getItem("lastUrlCalled");
const checkRequest = checkDuplicateRequestAndStoreRequest(config);
if (
config?.url === lastUrl &&
config?.url !== "/health" &&
config?.method === "get"
) {
return Promise.reject(`
Duplicate request detected.
URL: ${config.url}
Method: ${config.method}
`);
if (!checkRequest) {
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}`;

View file

@ -0,0 +1,30 @@
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../../constants/constants";
export function checkDuplicateRequestAndStoreRequest(config) {
const lastUrl = localStorage.getItem("lastUrlCalled");
const lastMethodCalled = localStorage.getItem("lastMethodCalled");
const lastRequestTime = localStorage.getItem("lastRequestTime");
const currentTime = Date.now();
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) =>
config?.url!.includes(request),
);
if (
config?.url === lastUrl &&
!isContained &&
lastMethodCalled === config.method &&
lastMethodCalled === "get" && // Assuming you want to check only for GET requests
lastRequestTime &&
currentTime - parseInt(lastRequestTime, 10) < 800
) {
return false;
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem("lastRequestTime", currentTime.toString());
return true;
}

View file

@ -1,7 +1,7 @@
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 { BASE_URL_API, MAX_BATCH_SIZE } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
APIObjectType,
@ -28,6 +28,7 @@ import {
UploadFileTypeAPI,
errorsTypeAPI,
} from "./../../types/api/index";
import { Message } from "../../types/messages";
/**
* Fetches all objects from the API endpoint.
@ -157,6 +158,7 @@ export async function updateFlowInDatabase(
data: updatedFlow.data,
description: updatedFlow.description,
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
endpoint_name: updatedFlow.endpoint_name,
});
if (response?.status !== 200) {
@ -1003,12 +1005,41 @@ export async function deleteFlowPool(
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
}
/**
* Deletes multiple flow components by their IDs.
* @param flowIds - An array of flow IDs to be deleted.
* @param token - The authorization token for the API request.
* @returns A promise that resolves to an array of AxiosResponse objects representing the delete responses.
*/
export async function multipleDeleteFlowsComponents(
flowIds: string[],
): Promise<AxiosResponse<any>> {
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
flow_ids: flowIds,
});
): Promise<AxiosResponse<any>[]> {
const batches: string[][] = [];
// Split the flowIds into batches
for (let i = 0; i < flowIds.length; i += MAX_BATCH_SIZE) {
batches.push(flowIds.slice(i, i + MAX_BATCH_SIZE));
}
// Function to delete a batch of flow IDs
const deleteBatch = async (batch: string[]): Promise<AxiosResponse<any>> => {
try {
return await api.delete(`${BASE_URL_API}flows/`, {
data: batch,
});
} catch (error) {
console.error("Error deleting flows:", error);
throw error;
}
};
// Execute all delete requests
const responses: Promise<AxiosResponse<any>>[] = batches.map((batch) =>
deleteBatch(batch),
);
// Return the responses after all requests are completed
return Promise.all(responses);
}
export async function getTransactionTable(
@ -1027,16 +1058,47 @@ export async function getTransactionTable(
}
export async function getMessagesTable(
id: string,
mode: "intersection" | "union",
id?: string,
excludedFields?: string[],
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
if (id) {
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);
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
return { rows: rows.data, columns };
}
export async function getSessions(id?: string): Promise<Array<string>> {
const config = {};
if (id) {
config["params"] = { flow_id: id };
}
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
const sessions = new Set<string>();
rows.data.forEach((row) => {
sessions.add(row.session_id);
});
return Array.from(sessions);
}
export async function deleteMessagesFn(ids: number[]) {
try {
return await api.delete(`${BASE_URL_API}monitor/messages`, {
data: ids,
});
} catch (error) {
console.error("Error deleting flows:", error);
throw error;
}
}
export async function updateMessageApi(data: Message) {
return await api.post(`${BASE_URL_API}monitor/messages/${data.index}`, data);
}

View file

@ -0,0 +1,32 @@
import axios from "axios";
import { BASE_URL_API } from "../../constants/constants";
/**
* Fetches the configuration data from the API.
* @returns {Promise<any>} A promise that resolves to the configuration data.
* @throws {Error} If there was an error fetching the configuration data.
*/
export async function fetchConfig() {
try {
const response = await axios.get(`${BASE_URL_API}config`);
return response.data;
} catch (error) {
console.error("Failed to fetch configuration:", error);
throw error;
}
}
/**
* Sets up default configurations for Axios.
* Fetches the timeout configuration and sets it as the default timeout for Axios requests.
*/
export async function setupAxiosDefaults() {
const config = await fetchConfig();
// Create Axios instance with the fetched timeout configuration
const timeoutInMilliseconds = config.frontend_timeout
? config.frontend_timeout * 1000
: 30000;
axios.defaults.baseURL = "";
axios.defaults.timeout = timeoutInMilliseconds;
}

View file

@ -0,0 +1 @@
export const TEXT_FIELD_TYPES: string[] = ["str", "SecretStr"];

View file

@ -46,6 +46,7 @@ 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";
import { TEXT_FIELD_TYPES } from "./constants";
export default function ParameterComponent({
left,
@ -88,7 +89,6 @@ export default function ParameterComponent({
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
);
@ -173,8 +173,6 @@ export default function ParameterComponent({
renderTooltips();
}, [tooltipTitle, flow]);
console.log(left === true && type === "dict");
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
@ -240,7 +238,7 @@ export default function ParameterComponent({
(left ? "" : " justify-end")
}
>
<Case condition={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>
@ -311,7 +309,7 @@ export default function ParameterComponent({
<Case
condition={
left === true &&
type === "str" &&
TEXT_FIELD_TYPES.includes(type ?? "") &&
!data.node?.template[name]?.options
}
>
@ -357,8 +355,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name]?.refresh_button_text ??
"Refresh"
data.node?.template[name].refresh_button_text
}
className="extra-side-bar-buttons mt-1"
handleUpdateValues={handleRefreshButtonPress}
@ -406,8 +403,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name]?.refresh_button_text ??
"Refresh"
data.node?.template[name].refresh_button_text
}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
@ -549,9 +545,7 @@ export default function ParameterComponent({
value={
!data.node!.template[name]?.value ||
data.node!.template[name]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
? {}
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}

View file

@ -10,7 +10,6 @@ import Loading from "../../components/ui/loading";
import { Textarea } from "../../components/ui/textarea";
import Xmark from "../../components/ui/xmark";
import {
NATIVE_CATEGORIES,
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
@ -28,9 +27,9 @@ import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, cn } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
data,
@ -77,18 +76,14 @@ export default function GenericNode({
// This one should run only once
// first check if data.type in NATIVE_CATEGORIES
// if not return
if (
!NATIVE_CATEGORIES.includes(types[data.type]) ||
!data.node?.template?.code?.value
)
return;
const thisNodeTemplate = templates[data.type].template;
if (!data.node?.template?.code?.value) return;
const thisNodeTemplate = templates[data.type]?.template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
if (!thisNodeTemplate?.code) return;
const currentCode = thisNodeTemplate.code?.value;
const thisNodesCode = data.node!.template?.code?.value;
const componentsToIgnore = ["Custom Component", "Prompt"];
const componentsToIgnore = ["Custom Component"];
if (
currentCode !== thisNodesCode &&
!componentsToIgnore.includes(data.node!.display_name)
@ -340,16 +335,20 @@ export default function GenericNode({
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
const names = classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div",
specificClassFromBuildStatus,
);
return names;
};
const getBaseBorderClass = (selected) =>
selected ? "border border-ring" : "border";
const getBaseBorderClass = (selected) => {
let className = selected ? "border border-ring" : "border";
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
return data.node?.frozen ? frozenClass : className;
};
const getNodeSizeClass = (showNode) =>
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
@ -423,6 +422,7 @@ export default function GenericNode({
"generic-node-title-arrangement rounded-full" +
(!showNode && " justify-center ")
}
data-testid="generic-node-title-arrangement"
>
{iconNodeRender()}
{showNode && (
@ -459,7 +459,7 @@ export default function GenericNode({
<div className="group flex items-start gap-1.5">
<ShadTooltip content={data.node?.display_name}>
<div
onDoubleClick={(event) => {
onClick={(event) => {
if (nameEditable) {
setInputName(true);
}
@ -473,21 +473,6 @@ export default function GenericNode({
{data.node?.display_name}
</div>
</ShadTooltip>
{nameEditable && (
<div
onClick={(event) => {
setInputName(true);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<IconComponent
name="PencilLine"
className="hidden h-3 w-3 text-status-blue group-hover:block"
/>
</div>
)}
</div>
)}
</div>
@ -725,14 +710,14 @@ export default function GenericNode({
) : (
<div
className={cn(
"generic-node-desc-text truncate-multiline word-break-break-word",
"generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable
? "font-light italic"
: "",
)}
onDoubleClick={(e) => {
onClick={(e) => {
setInputDescription(true);
takeSnapshot();
}}

View file

@ -40,7 +40,7 @@ const useFetchDataOnMount = (
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
list: [responseError?.response?.data?.detail ?? "Unknown error"],
});
}
setIsLoading(false);

View file

@ -10,7 +10,6 @@ const useHandleOnNewValue = (
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -45,7 +44,9 @@ const useHandleOnNewValue = (
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
list: [
responseError?.response?.data?.detail.error ?? "Unknown error",
],
});
}
setIsLoading(false);

View file

@ -26,7 +26,7 @@ const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
list: [responseError?.response?.data?.detail ?? "Unknown error"],
});
}
setIsLoading(false);

View file

@ -1,10 +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;
}
template: APITemplateType,
templateField: string,
): string {
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name ?? templateField;
}

View file

@ -1,40 +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);
// 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);
}

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { useLocationStore } from "../stores/locationStore";
function useTrackLastVisitedPath() {
const location = useLocation();
const setHistory = useLocationStore((state) => state.setRouteHistory);
useEffect(() => {
setHistory(location.pathname);
}, [location]);
}
export default useTrackLastVisitedPath;

View file

@ -0,0 +1,25 @@
export default function SvgStreamlit(props) {
return (
<svg
width="301"
height="165"
viewBox="0 0 301 165"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M150.731 101.547L98.1387 73.7471L6.84674 25.4969C6.7634 25.4136 6.59674 25.4136 6.51341 25.4136C3.18007 23.8303 -0.236608 27.1636 1.0134 30.497L47.5302 149.139L47.5385 149.164C47.5885 149.281 47.6302 149.397 47.6802 149.514C49.5885 153.939 53.7552 156.672 58.2886 157.747C58.6719 157.831 58.9461 157.906 59.4064 157.998C59.8645 158.1 60.5052 158.239 61.0552 158.281C61.1469 158.289 61.2302 158.289 61.3219 158.297H61.3886C61.4552 158.306 61.5219 158.306 61.5886 158.314H61.6802C61.7386 158.322 61.8052 158.322 61.8636 158.322H61.9719C62.0386 158.331 62.1052 158.331 62.1719 158.331V158.331C121.084 164.754 180.519 164.754 239.431 158.331V158.331C240.139 158.331 240.831 158.297 241.497 158.231C241.714 158.206 241.922 158.181 242.131 158.156C242.156 158.147 242.189 158.147 242.214 158.139C242.356 158.122 242.497 158.097 242.639 158.072C242.847 158.047 243.056 158.006 243.264 157.964C243.681 157.872 243.87 157.806 244.436 157.611C245.001 157.417 245.94 157.077 246.527 156.794C247.115 156.511 247.522 156.239 248.014 155.931C248.622 155.547 249.201 155.155 249.788 154.715C250.041 154.521 250.214 154.397 250.397 154.222L250.297 154.164L150.731 101.547Z"
fill="#FF4B4B"
/>
<path
d="M294.766 25.4981H294.683L203.357 73.7483L254.124 149.357L300.524 30.4981V30.3315C301.691 26.8314 298.108 23.6648 294.766 25.4981"
fill="#7D353B"
/>
<path
d="M155.598 2.55572C153.264 -0.852624 148.181 -0.852624 145.931 2.55572L98.1389 73.7477L150.731 101.548L250.398 154.222C251.024 153.609 251.526 153.012 252.056 152.381C252.806 151.456 253.506 150.465 254.123 149.356L203.356 73.7477L155.598 2.55572Z"
fill="#BD4043"
/>
</svg>
);
}

View file

@ -0,0 +1,8 @@
import React, { forwardRef } from "react";
import SvgStreamlit from "./SvgStreamlit";
export const Streamlit = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgStreamlit className="icon" ref={ref} {...props} />;
},
);

View file

@ -84,7 +84,6 @@ export default function IOFileInput({ field, updateValue }: IOFileInputProps) {
uploadFile(file, currentFlowId)
.then((res) => res.data)
.then((data) => {
console.log("File uploaded successfully");
// Get the file name from the response
const { file_path, flowId } = data;
setFilePath(file_path);

View file

@ -10,11 +10,11 @@ import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
import { chatViewProps, FilePreviewType } from "../../../../types/components";
import { FilePreviewType, chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput";
import ChatMessage from "./chatMessage";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import ChatMessage from "./chatMessage";
export default function ChatView({
sendMessage,
@ -116,10 +116,21 @@ export default function ChatView({
if (lockChat) setLockChat(false);
}
function handleSelectChange(event: string): void {
switch (event) {
case "builds":
clearChat();
break;
case "buildsNSession":
console.log("delete build and session");
break;
}
}
function updateChat(
chat: ChatMessageType,
message: string,
stream_url?: string,
stream_url?: string
) {
// if (message === "") return;
chat.message = message;
@ -148,7 +159,7 @@ export default function ChatView({
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
setIsDragging,
setFiles,
currentFlowId,
currentFlowId
);
return (
@ -161,18 +172,59 @@ export default function ChatView({
>
<div className="eraser-size">
<div className="eraser-position">
<button disabled={lockChat} onClick={() => clearChat()}>
<button
className="flex gap-1"
onClick={() => handleSelectChange("builds")}
>
<IconComponent
name="Eraser"
className={classNames(
"h-5 w-5",
lockChat
? "animate-pulse text-primary"
: "text-primary hover:text-gray-600",
: "text-primary hover:text-gray-600"
)}
aria-hidden="true"
/>
</button>
{/* <Select
onValueChange={handleSelectChange}
value=""
disabled={lockChat}
>
<SelectTrigger className="">
<button className="flex gap-1">
<IconComponent
name="Eraser"
className={classNames(
"h-5 w-5 transition-all duration-100",
lockChat ? "animate-pulse text-primary" : "text-primary",
)}
aria-hidden="true"
/>
</button>
</SelectTrigger>
<SelectContent className="right-[9.5em]">
<SelectItem value="builds" className="cursor-pointer">
<div className="flex">
<IconComponent
name={"Trash2"}
className={`relative top-0.5 mr-2 h-4 w-4`}
/>
<span className="">Clear Builds</span>
</div>
</SelectItem>
<SelectItem value="buildsNSession" className="cursor-pointer">
<div className="flex">
<IconComponent
name={"Trash2"}
className={`relative top-0.5 mr-2 h-4 w-4`}
/>
<span className="">Clear Builds & Session</span>
</div>
</SelectItem>
</SelectContent>
</Select> */}
</div>
<div ref={messagesRef} className="chat-message-div">
{chatHistory?.length > 0 ? (

View file

@ -3,7 +3,6 @@ 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 {
Tabs,
TabsContent,
@ -34,25 +33,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() {
@ -78,6 +77,7 @@ export default function IOModal({
const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode);
const [sessions, setSessions] = useState<string[]>([]);
async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null);
@ -89,7 +89,13 @@ export default function IOModal({
// }
// }, [open, currentFlow]);
async function sendMessage({repeat=1,files}:{repeat:number,files?:string[]}): Promise<void> {
async function sendMessage({
repeat = 1,
files,
}: {
repeat: number;
files?: string[];
}): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
@ -98,7 +104,8 @@ export default function IOModal({
await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
files:files
files: files,
silent: true,
}).catch((err) => {
console.error(err);
setLockChat(false);
@ -120,6 +127,11 @@ export default function IOModal({
useEffect(() => {
setSelectedViewField(startView());
// if (haveChat) {
// getSessions().then((sessions) => {
// setSessions(sessions);
// });
// }
}, [open]);
return (
@ -128,6 +140,7 @@ export default function IOModal({
open={open}
setOpen={setOpen}
disable={disable}
onSubmit={() => sendMessage({ repeat: 1 })}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
@ -147,7 +160,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
@ -167,6 +180,9 @@ export default function IOModal({
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
{/* {haveChat && (
<TabsTrigger value={"3"}>History</TabsTrigger>
)} */}
</TabsList>
</div>
@ -180,11 +196,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 +264,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
@ -260,6 +276,10 @@ export default function IOModal({
key={index}
>
<AccordionComponent
disabled={
node.data.node!.template["input_value"]
?.value === ""
}
trigger={
<div className="file-component-badge-div">
<ShadTooltip
@ -315,7 +335,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 +354,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 +376,7 @@ export default function IOModal({
<div
className={cn(
"flex h-full w-full",
selectedViewField ? "hidden" : "",
selectedViewField ? "hidden" : ""
)}
>
{haveChat ? (
@ -378,26 +398,22 @@ export default function IOModal({
</div>
</BaseModal.Content>
{!haveChat ? (
<BaseModal.Footer>
<div className="flex w-full justify-end pt-2">
<Button
variant={"outline"}
className="flex gap-2 px-3"
onClick={() => sendMessage({repeat:1})}
>
<BaseModal.Footer
submit={{
label: "Run Flow",
icon: (
<IconComponent
name={isBuilding ? "Loader2" : "Zap"}
className={cn(
"h-4 w-4",
isBuilding
? "animate-spin"
: "fill-current text-medium-indigo",
: "fill-current text-medium-indigo"
)}
/>
Run Flow
</Button>
</div>
</BaseModal.Footer>
),
}}
/>
) : (
<></>
)}

View file

@ -4,23 +4,45 @@
* @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code
*/
export default function getCurlCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
}/api/v1/run/${flowId}?stream=false \\
export function getCurlRunCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
endpointName?: string,
): string {
const tweaksObject = tweaksBuildedObject[0];
// show the endpoint name in the curl command if it exists
return `curl -X POST \\
"${window.location.protocol}//${window.location.host}/api/v1/run/${
endpointName || flowId
}?stream=false" \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}'
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}}'
`;
}
/**
* Generates a cURL command for making a POST request to a webhook endpoint.
*
* @param {Object} options - The options for generating the cURL command.
* @param {string} options.flowId - The ID of the flow.
* @param {boolean} options.isAuth - Indicates whether authentication is required.
* @param {string} options.endpointName - The name of the webhook endpoint.
* @returns {string} The cURL command.
*/
export function getCurlWebhookCode(flowId, isAuth, endpointName?: string) {
return `curl -X POST \\
"${window.location.protocol}//${window.location.host}/api/v1/webhook/${
endpointName || flowId
}" \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"any": "data"}'
`;
}

View file

@ -6,53 +6,102 @@
* @returns {string} - The python code
*/
export default function getPythonApiCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
from typing import Optional
BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str,
flow_id: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {
"input_value": message,
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
message = "message"
${!isAuth ? `api_key = "<your api key>"` : ""}
print(run_flow(message=message, flow_id=FLOW_ID, tweaks=TWEAKS${
!isAuth ? `, api_key=api_key` : ""
}))`;
}
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
endpointName?: string,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import argparse
import json
from argparse import RawTextHelpFormatter
import requests
from typing import Optional
import warnings
try:
from langflow.load import upload_file
except ImportError:
warnings.warn("Langflow provides a function to help you upload files to the flow. Please install langflow to use it.")
upload_file = None
BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
ENDPOINT = "${endpointName || ""}" ${
endpointName
? `# The endpoint name of the flow`
: `# You can set a specific endpoint name in the flow settings`
}
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str,
endpoint: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param endpoint: The ID or the endpoint name of the flow
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{endpoint}"
payload = {
"input_value": message,
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
def main():
parser = argparse.ArgumentParser(description="""Run a flow with a given message and optional tweaks.\nRun it like: python <your file>.py "your message here" --endpoint "your_endpoint" --tweaks '{"key": "value"}'""",
formatter_class=RawTextHelpFormatter)
parser.add_argument("message", type=str, help="The message to send to the flow")
parser.add_argument("--endpoint", type=str, default=ENDPOINT or FLOW_ID, help="The ID or the endpoint name of the flow")
parser.add_argument("--tweaks", type=str, help="JSON string representing the tweaks to customize the flow", default=json.dumps(TWEAKS))
parser.add_argument("--api_key", type=str, help="API key for authentication", default=None)
parser.add_argument("--output_type", type=str, default="chat", help="The output type")
parser.add_argument("--input_type", type=str, default="chat", help="The input type")
parser.add_argument("--upload_file", type=str, help="Path to the file to upload", default=None)
parser.add_argument("--components", type=str, help="Components to upload the file to", default=None)
args = parser.parse_args()
try:
tweaks = json.loads(args.tweaks)
except json.JSONDecodeError:
raise ValueError("Invalid tweaks JSON string")
if args.upload_file:
if not upload_file:
raise ImportError("Langflow is not installed. Please install it to use the upload_file function.")
elif not args.components:
raise ValueError("You need to provide the components to upload the file to.")
tweaks = upload_file(file_path=args.upload_file, host=BASE_API_URL, flow_id=ENDPOINT, components=args.components, tweaks=tweaks)
response = run_flow(
message=args.message,
endpoint=args.endpoint,
output_type=args.output_type,
input_type=args.input_type,
tweaks=tweaks,
api_key=args.api_key
)
print(json.dumps(response, indent=2))
if __name__ == "__main__":
main()
`;
}

View file

@ -4,14 +4,17 @@
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export default function getPythonCode(flowName: string, tweaksBuildedObject): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}
export default function getPythonCode(
flowName: string,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}

View file

@ -4,12 +4,12 @@
* @returns {string} - The widget code
*/
export default function getWidgetCode(
flowId: string,
flowName: string,
isAuth: boolean,
): string {
return `<script src="https://cdn.jsdelivr.net/gh/langflow-ai/langflow-embedded-chat@1.0_alpha/dist/build/static/js/bundle.min.js"></script>
flowId: string,
flowName: string,
isAuth: boolean,
): string {
return `<script src="https://cdn.jsdelivr.net/gh/langflow-ai/langflow-embedded-chat@1.0_alpha/dist/build/static/js/bundle.min.js"></script>
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
@ -19,6 +19,6 @@ export default function getWidgetCode(
api_key="..."`
: ""
}
></langflow-chat>`;
}
}

View file

@ -1,78 +1,61 @@
export default function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
];
}
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
language: "py",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[4],
},
];
export function createTabsArray(
codes,
includeWebhookCurl = false,
includeTweaks = false,
) {
const tabs = [
{
name: "Run cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[2],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "html",
code: codes[4],
},
];
if (includeWebhookCurl) {
tabs.splice(1, 0, {
name: "Webhook cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[1],
});
}
if (includeTweaks) {
tabs.push({
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[5],
});
}
return tabs;
}

View file

@ -18,13 +18,13 @@ import { buildContent } from "../utils/build-content";
import { buildTweaks } from "../utils/build-tweaks";
import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object";
import { getChangesType } from "../utils/get-changes-types";
import { getCurlRunCode, getCurlWebhookCode } from "../utils/get-curl-code";
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
import { getValue } from "../utils/get-value";
import getPythonApiCode from "../utils/get-python-api-code";
import getCurlCode from "../utils/get-curl-code";
import getPythonCode from "../utils/get-python-code";
import { getValue } from "../utils/get-value";
import getWidgetCode from "../utils/get-widget-code";
import tabsArray from "../utils/tabs-array";
import { createTabsArray } from "../utils/tabs-array";
const ApiModal = forwardRef(
(
@ -46,19 +46,39 @@ const ApiModal = forwardRef(
const { autoLogin } = useContext(AuthContext);
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState("0");
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak);
const curl_code = getCurlCode(flow?.id, autoLogin, tweak);
const pythonApiCode = getPythonApiCode(
flow?.id,
autoLogin,
tweak,
flow?.endpoint_name,
);
const curl_run_code = getCurlRunCode(
flow?.id,
autoLogin,
tweak,
flow?.endpoint_name,
);
const curl_webhook_code = getCurlWebhookCode(
flow?.id,
autoLogin,
flow?.endpoint_name,
);
const pythonCode = getPythonCode(flow?.name, tweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
console.log("flow", flow);
const includeWebhook = flow.webhook;
const tweaksCode = buildTweaks(flow);
const codesArray = [
curl_code,
curl_run_code,
curl_webhook_code,
pythonApiCode,
pythonCode,
widgetCode,
pythonCode,
];
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
const [tabs, setTabs] = useState(
createTabsArray(codesArray, includeWebhook),
);
const canShowTweaks =
flow &&
@ -88,9 +108,9 @@ const ApiModal = forwardRef(
if (Object.keys(tweaksCode).length > 0) {
setActiveTab("0");
setTabs(tabsArray(codesArray, 1));
setTabs(createTabsArray(codesArray, includeWebhook, true));
} else {
setTabs(tabsArray(codesArray, 1));
setTabs(createTabsArray(codesArray, includeWebhook, true));
}
}, [flow["data"]!["nodes"], open]);
@ -161,7 +181,12 @@ const ApiModal = forwardRef(
const addCodes = (cloneTweak) => {
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak);
const curl_code = getCurlCode(flow?.id, autoLogin, cloneTweak);
const curl_code = getCurlRunCode(
flow?.id,
autoLogin,
cloneTweak,
flow?.endpoint_name,
);
const pythonCode = getPythonCode(flow?.name, cloneTweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);

View file

@ -0,0 +1,67 @@
export const switchCaseModalSize = (size: string) => {
let minWidth: string;
let height: string;
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
height = "h-[40vh]";
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
height = "h-[60vh]";
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
height = "h-[80vh]";
break;
case "three-cards":
minWidth = "min-w-[1066px]";
height = "h-fit";
break;
case "large-thin":
minWidth = "min-w-[65vw]";
height = "h-[80vh]";
break;
case "md-thin":
minWidth = "min-w-[85vw]";
height = "h-[70vh]";
break;
case "sm-thin":
minWidth = "min-w-[65vw]";
height = "h-[70vh]";
break;
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
}
return { minWidth, height };
};

View file

@ -15,8 +15,11 @@ import {
DialogContent as ModalContent,
} from "../../components/ui/dialog-with-no-close";
import { DialogClose } from "@radix-ui/react-dialog";
import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
@ -61,8 +64,38 @@ const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
);
};
const Footer: React.FC<{ children: ReactNode }> = ({ children }) => {
return <>{children}</>;
const Footer: React.FC<{
children?: ReactNode;
submit?: {
label: string;
icon?: ReactNode;
loading?: boolean;
disabled?: boolean;
dataTestId?: string;
};
}> = ({ children, submit }) => {
return submit ? (
<div className="flex w-full items-center justify-between">
{children ?? <div />}
<div className="flex items-center gap-3">
<DialogClose asChild>
<Button variant="outline" type="button">
Cancel
</Button>
</DialogClose>
<Button
data-testid={submit.dataTestId}
type="submit"
loading={submit.loading}
>
{submit.icon && submit.icon}
{submit.label}
</Button>
</div>
</div>
) : (
<>{children && children}</>
);
};
interface BaseModalProps {
children: [
@ -91,6 +124,7 @@ interface BaseModalProps {
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
type?: "modal" | "dialog";
onSubmit?: () => void;
}
function BaseModal({
open,
@ -99,6 +133,7 @@ function BaseModal({
size = "large",
onChangeOpenModal,
type = "dialog",
onSubmit,
}: BaseModalProps) {
const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header,
@ -113,71 +148,7 @@ function BaseModal({
(child) => (child as React.ReactElement).type === Footer,
);
let minWidth: string;
let height: string;
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
height = "h-[40vh]";
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
height = "h-[60vh]";
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
height = "h-[80vh]";
break;
case "three-cards":
minWidth = "min-w-[1066px]";
height = "h-fit";
break;
case "large-thin":
minWidth = "min-w-[65vw]";
height = "h-[80vh]";
break;
case "md-thin":
minWidth = "min-w-[85vw]";
height = "h-[70vh]";
break;
case "sm-thin":
minWidth = "min-w-[65vw]";
height = "h-[70vh]";
break;
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
}
let { minWidth, height } = switchCaseModalSize(size);
useEffect(() => {
if (onChangeOpenModal) {
@ -212,13 +183,34 @@ function BaseModal({
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
{onSubmit ? (
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
className="flex flex-col gap-6"
>
<div
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</form>
) : (
<>
<div
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</>
)}
</DialogContent>
</Dialog>

View file

@ -59,7 +59,7 @@ export default function DeleteConfirmationModal({
<DialogClose asChild>
<Button
onClick={(e) => e.stopPropagation()}
className="mr-3"
className="mr-1"
variant="outline"
>
Cancel

View file

@ -29,7 +29,7 @@ export default function DictAreaModal({
useEffect(() => {
if (value) ref.current = value;
}, [ref]);
}, [value]);
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>

View file

@ -15,7 +15,6 @@ import ShadTooltip from "../../components/shadTooltipComponent";
import TextAreaComponent from "../../components/textAreaComponent";
import ToggleShadComponent from "../../components/toggleShadComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Table,
TableBody,
@ -43,19 +42,23 @@ import BaseModal from "../baseModal";
const EditNodeModal = forwardRef(
(
{
data,
nodeLength,
open,
setOpen,
data,
}: {
data: NodeDataType;
nodeLength: number;
open: boolean;
setOpen: (open: boolean) => void;
data: NodeDataType;
},
ref,
) => {
const [myData, setMyData] = useState(data);
const nodes = useFlowStore((state) => state.nodes);
const dataFromStore = nodes.find((node) => node.id === node.id)?.data;
const [myData, setMyData] = useState(dataFromStore ?? data);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
@ -98,6 +101,16 @@ const EditNodeModal = forwardRef(
onChangeOpenModal={(open) => {
setMyData(data);
}}
onSubmit={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: myData.node,
},
}));
setOpen(false);
}}
>
<BaseModal.Trigger>
<></>
@ -130,6 +143,7 @@ const EditNodeModal = forwardRef(
<TableHeader className="edit-node-modal-table-header">
<TableRow className="">
<TableHead className="h-7 text-center">PARAM</TableHead>
<TableHead className="h-7 text-center">DESC</TableHead>
<TableHead className="h-7 p-0 text-center">
VALUE
</TableHead>
@ -184,11 +198,17 @@ const EditNodeModal = forwardRef(
>
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
styleClasses="z-50"
content={
myData.node?.template[templateParam].proxy
? myData.node?.template[templateParam]
.proxy?.id
: null
: myData.node?.template[templateParam]
.display_name
? myData.node!.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name
}
>
<span>
@ -201,6 +221,20 @@ const EditNodeModal = forwardRef(
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
styleClasses="z-50"
content={
data.node?.template[templateParam]?.info ??
null
}
>
<span>
{data.node?.template[templateParam]?.info ??
""}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
<Case
condition={
@ -297,9 +331,7 @@ const EditNodeModal = forwardRef(
myData.node!.template[
templateParam
]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
? {}
: myData.node!.template[templateParam]
.value
}
@ -608,26 +640,7 @@ const EditNodeModal = forwardRef(
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button
data-test-id="saveChangesBtn"
id={"saveChangesBtn"}
className="mt-3"
onClick={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: myData.node,
},
}));
setOpen(false);
}}
type="submit"
>
Save Changes
</Button>
</BaseModal.Footer>
<BaseModal.Footer submit={{ label: "Save Changes" }} />
</BaseModal>
);
},

View file

@ -1,7 +1,6 @@
import { ReactNode, forwardRef, useEffect, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
import {
@ -19,7 +18,7 @@ const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const version = useDarkStore((state) => state.version);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
const [checked, setChecked] = useState(true);
const [checked, setChecked] = useState(false);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
useEffect(() => {
setName(currentFlow!.name);
@ -30,7 +29,43 @@ const ExportModal = forwardRef(
const [open, setOpen] = useState(false);
return (
<BaseModal size="smaller-h-full" open={open} setOpen={setOpen}>
<BaseModal
size="smaller-h-full"
open={open}
setOpen={setOpen}
onSubmit={() => {
if (checked) {
downloadFlow(
{
id: currentFlow!.id,
data: currentFlow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!,
description,
);
setNoticeData({
title: API_WARNING_NOTICE_ALERT,
});
} else
downloadFlow(
removeApiKeys({
id: currentFlow!.id,
data: currentFlow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!,
description,
);
setOpen(false);
}}
>
<BaseModal.Trigger asChild>{props.children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
<span className="pr-2">Export</span>
@ -64,47 +99,9 @@ const ExportModal = forwardRef(
</span>
</BaseModal.Content>
<BaseModal.Footer>
<Button
onClick={() => {
if (checked) {
downloadFlow(
{
id: currentFlow!.id,
data: currentFlow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!,
description
);
setNoticeData({
title: API_WARNING_NOTICE_ALERT,
});
} else
downloadFlow(
removeApiKeys({
id: currentFlow!.id,
data: currentFlow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!,
description
);
setOpen(false);
}}
type="submit"
>
Download Flow
</Button>
</BaseModal.Footer>
<BaseModal.Footer submit={{ label: "Download Flow" }} />
</BaseModal>
);
}
},
);
export default ExportModal;

View file

@ -1,43 +1,29 @@
import { useEffect, useState } from "react";
import { ColDef, ColGroupDef } from "ag-grid-community";
import { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import TableComponent from "../../components/tableComponent";
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import { FlowType, NodeDataType } from "../../types/flow";
import BaseModal from "../baseModal";
import TableComponent from "../../components/tableComponent";
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
import {
ColDef,
ColGroupDef,
SizeColumnsToFitGridStrategy,
} from "ag-grid-community";
export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const nodes = useFlowStore((state) => state.nodes);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const flows = useFlowsManagerStore((state) => state.flows);
useEffect(() => {
setName(currentFlow!.name);
setDescription(currentFlow!.description);
}, [currentFlow!.name, currentFlow!.description, open]);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
const [name, setName] = useState(currentFlow!.name);
const [description, setDescription] = useState(currentFlow!.description);
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]);
const [activeTab, setActiveTab] = useState("Executions");
function handleClick(): void {
currentFlow!.name = name;
currentFlow!.description = description;
saveFlow(currentFlow!);
setOpen(false);
}
const noticed = useRef(false);
useEffect(() => {
if (activeTab === "Executions") {
@ -47,24 +33,35 @@ export default function FlowLogsModal({
setRows(rows);
});
} else if (activeTab === "Messages") {
getMessagesTable(currentFlowId, "union").then((data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
getMessagesTable("union", currentFlowId, ["index", "flow_id"]).then(
(data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
},
);
}
if (open && activeTab === "Messages" && !noticed.current) {
const haStream = nodes
.map((nodes) => (nodes.data as NodeDataType).node!.template)
.some((template) => template["stream"] && template["stream"].value);
console.log(
haStream,
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template),
);
if (haStream) {
setNoticeData({
title: "Streamed messages will not appear in this table.",
});
noticed.current = true;
}
}
if (!open) {
noticed.current = false;
}
}, [open, activeTab]);
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== currentFlow!.name));
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="large">
<BaseModal.Header description="Inspect component executions and monitor sent messages in the playground.">

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
@ -22,12 +22,25 @@ export default function FlowSettingsModal({
const [name, setName] = useState(currentFlow!.name);
const [description, setDescription] = useState(currentFlow!.description);
const [endpoint_name, setEndpointName] = useState(currentFlow!.endpoint_name);
const [isSaving, setIsSaving] = useState(false);
function handleClick(): void {
setIsSaving(true);
currentFlow!.name = name;
currentFlow!.description = description;
saveFlow(currentFlow!);
setOpen(false);
currentFlow!.endpoint_name = endpoint_name;
saveFlow(currentFlow!)
?.then(() => {
setOpen(false);
setIsSaving(false);
})
.catch((err) => {
useAlertStore.getState().setErrorData({
title: "Error while saving changes",
list: [err?.response?.data.detail ?? ""],
});
console.error(err);
});
}
const [nameLists, setNameList] = useState<string[]>([]);
@ -41,7 +54,12 @@ export default function FlowSettingsModal({
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="smaller">
<BaseModal
open={open}
setOpen={setOpen}
size="smaller-h-full"
onSubmit={handleClick}
>
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
<span className="pr-2">Settings</span>
<IconComponent name="Settings2" className="mr-2 h-4 w-4 " />
@ -51,20 +69,21 @@ export default function FlowSettingsModal({
invalidNameList={nameLists}
name={name}
description={description}
endpointName={endpoint_name}
setName={setName}
setDescription={setDescription}
setEndpointName={setEndpointName}
/>
</BaseModal.Content>
<BaseModal.Footer>
<Button
disabled={nameLists.includes(name) && name !== currentFlow!.name}
onClick={handleClick}
type="submit"
>
Save
</Button>
</BaseModal.Footer>
<BaseModal.Footer
submit={{
label: "Save",
disabled: nameLists.includes(name) && name !== currentFlow!.name,
dataTestId: "save-flow-settings",
loading: isSaving,
}}
/>
</BaseModal>
);
}

View file

@ -166,7 +166,6 @@ export default function GenericModal({
}
})
.catch((error) => {
console.log(error);
setIsEdit(true);
return setErrorData({
title: PROMPT_ERROR_ALERT,

View file

@ -147,6 +147,7 @@ export default function SecretKeyModal({
{renderKey === false && (
<div className="float-right">
<Button
type="button"
className="mr-3"
variant="outline"
onClick={() => {

View file

@ -1,4 +1,3 @@
import { Loader2 } from "lucide-react";
import { ReactNode, useEffect, useMemo, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
@ -129,14 +128,14 @@ export default function ShareModal({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
},
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
unavaliableNames.find((e) => e.name === name)!.id,
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
@ -202,6 +201,18 @@ export default function ShareModal({
size="smaller-h-full"
open={(!disabled && open) ?? internalOpen}
setOpen={setOpen ?? internalSetOpen}
onSubmit={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name,
);
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
} else {
setOpenConfirmationModal(true);
}
}}
>
<BaseModal.Trigger asChild>
{children ? children : <></>}
@ -250,8 +261,13 @@ export default function ShareModal({
</span>
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full justify-between gap-2">
<BaseModal.Footer
submit={{
label: `Share ${is_component ? " Component" : " Flow"}`,
loading: loadingNames,
}}
>
<>
{!is_component && (
<ExportModal>
<Button
@ -281,37 +297,7 @@ export default function ShareModal({
Export
</Button>
)}
<Button
disabled={loadingNames}
type="button"
className={is_component ? "w-40" : "w-28"}
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
);
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
} else {
setOpenConfirmationModal(true);
}
}}
>
{loadingNames ? (
<>
<div className="center">
<Loader2 className="m-auto h-4 w-4 animate-spin"></Loader2>
</div>
</>
) : (
<>
Share{" "}
{!loadingNames && (!is_component ? "Flow" : "Component")}
</>
)}
</Button>
</div>
</>
</BaseModal.Footer>
</BaseModal>
<>{modalConfirmation}</>

View file

@ -1,8 +1,8 @@
export default function getTagsIds(
tags: string[],
tagListId: { name: string; id: string }[],
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
tags: string[],
tagListId: { name: string; id: string }[],
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
}

View file

@ -1,145 +0,0 @@
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import {
API_ERROR_ALERT,
API_SUCCESS_ALERT,
} from "../../constants/alerts_constants";
import {
CREATE_API_KEY,
INSERT_API_KEY,
INVALID_API_KEY,
NO_API_KEY,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { addApiKeyStore } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { useStoreStore } from "../../stores/storeStore";
import { StoreApiKeyType } from "../../types/components";
import BaseModal from "../baseModal";
export default function StoreApiKeyModal({
children,
disabled = false,
}: StoreApiKeyType) {
if (disabled) return <>{children}</>;
const [open, setOpen] = useState(false);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { storeApiKey } = useContext(AuthContext);
const [apiKeyValue, setApiKeyValue] = useState("");
const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const setHasApiKey = useStoreStore((state) => state.updateHasApiKey);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const setLoadingApiKey = useStoreStore((state) => state.updateLoadingApiKey);
const handleSaveKey = () => {
if (apiKeyValue) {
addApiKeyStore(apiKeyValue).then(
() => {
setSuccessData({
title: API_SUCCESS_ALERT,
});
storeApiKey(apiKeyValue);
setOpen(false);
setHasApiKey(true);
setValidApiKey(true);
setLoadingApiKey(false);
},
(error) => {
setErrorData({
title: API_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
setHasApiKey(false);
setValidApiKey(false);
setLoadingApiKey(false);
}
);
}
};
return (
<BaseModal size="small-h-full" open={open && !disabled} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
(hasApiKey && !validApiKey
? INVALID_API_KEY
: !hasApiKey
? NO_API_KEY
: "") + INSERT_API_KEY
}
>
<span className="pr-2">API Key</span>
<IconComponent
name="Key"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
handleSaveKey();
}}
>
<div className="grid gap-5">
<Form.Field name="apikey">
<div className="flex items-center justify-between gap-2">
<Form.Control asChild>
<Input
//fake api key
value={apiKeyValue}
type="password"
onChange={({ target: { value } }) => {
setApiKeyValue(value);
}}
placeholder="Insert your API Key"
/>
</Form.Control>
</div>
</Form.Field>
</div>
<div className="flex items-end justify-between">
<span className="pr-1 text-xs text-muted-foreground">
{CREATE_API_KEY}{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
<div className="">
<Button
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Form.Submit asChild>
<Button
data-testid="api-key-save-button-store"
className="mt-8"
>
Save
</Button>
</Form.Submit>
</div>
</div>
</Form.Root>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -3,33 +3,33 @@ import { getRandomElement } from "../../../../../utils/reactflowUtils";
import { toTitleCase } from "../../../../../utils/utils";
export default function getRandomName(
retry: number = 0,
noSpace: boolean = false,
maxRetries: number = 3,
): string {
const left: string[] = ADJECTIVES;
const right: string[] = NOUNS;
const lv = getRandomElement(left);
const rv = getRandomElement(right);
// Condition to avoid "boring wozniak"
if (lv === "boring" && rv === "wozniak") {
if (retry < maxRetries) {
return getRandomName(retry + 1, noSpace, maxRetries);
} else {
console.warn("Max retries reached, returning as is");
}
retry: number = 0,
noSpace: boolean = false,
maxRetries: number = 3,
): string {
const left: string[] = ADJECTIVES;
const right: string[] = NOUNS;
const lv = getRandomElement(left);
const rv = getRandomElement(right);
// Condition to avoid "boring wozniak"
if (lv === "boring" && rv === "wozniak") {
if (retry < maxRetries) {
return getRandomName(retry + 1, noSpace, maxRetries);
} else {
console.warn("Max retries reached, returning as is");
}
// Append a suffix if retrying and noSpace is true
if (retry > 0 && noSpace) {
const retrySuffix = Math.floor(Math.random() * 10);
return `${lv}_${rv}${retrySuffix}`;
}
// Construct the final name
let final_name = noSpace ? `${lv}_${rv}` : `${lv} ${rv}`;
// Return title case final name
return toTitleCase(final_name);
}
}
// Append a suffix if retrying and noSpace is true
if (retry > 0 && noSpace) {
const retrySuffix = Math.floor(Math.random() * 10);
return `${lv}_${rv}${retrySuffix}`;
}
// Construct the final name
let final_name = noSpace ? `${lv}_${rv}` : `${lv} ${rv}`;
// Return title case final name
return toTitleCase(final_name);
}

View file

@ -19,10 +19,7 @@ import {
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import {
classNames,
removeCountFromString,
} from "../../../../utils/utils";
import { classNames, removeCountFromString } from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import ParentDisclosureComponent from "../ParentDisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
@ -45,7 +42,7 @@ export default function ExtraSidebar(): JSX.Element {
const [search, setSearch] = useState("");
function onDragStart(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType }
data: { type: string; node?: APIClassType },
): void {
//start drag event
var crt = event.currentTarget.cloneNode(true);
@ -71,7 +68,7 @@ export default function ExtraSidebar(): JSX.Element {
let keys = Object.keys(data[d]).filter(
(nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase()),
);
keys.forEach((element) => {
ret[d][element] = data[d][element];
@ -138,7 +135,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -175,7 +172,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -204,7 +201,7 @@ export default function ExtraSidebar(): JSX.Element {
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: ""
: "",
)}
>
<IconComponent
@ -213,14 +210,14 @@ export default function ExtraSidebar(): 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],
);
const ExportMemo = useMemo(
@ -231,7 +228,7 @@ export default function ExtraSidebar(): JSX.Element {
</button>
</ExportModal>
),
[]
[],
);
const getIcon = useMemo(() => {
@ -287,7 +284,7 @@ export default function ExtraSidebar(): JSX.Element {
<div className="side-bar-components-div-arrangement">
<div className="parent-disclosure-arrangement">
<div className="flex items-center gap-4 align-middle">
<span className="parent-disclosure-title">Core Components</span>
<span className="parent-disclosure-title">Basic Components</span>
</div>
</div>
{Object.keys(dataFilter)
@ -315,8 +312,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -360,13 +357,13 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}{" "}
<ParentDisclosureComponent
openDisc={false}
key={"Extended"}
key={"Advanced"}
button={{
title: "Extended",
title: "Advanced",
Icon: nodeIconsLucide.unknown,
}}
testId="extended-disclosure"
@ -397,8 +394,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -472,7 +469,7 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}
</ParentDisclosureComponent>
</div>

View file

@ -1,25 +1,24 @@
export default function sensitiveSort(a: string, b: string): number {
// Extract the name and number from each string using regular expressions
const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex);
const matchB = b.match(regex);
if (matchA && matchB) {
// Compare the names alphabetically
const nameA = matchA[1];
const nameB = matchB[1];
if (nameA !== nameB) {
return nameA.localeCompare(nameB);
}
// If the names are the same, compare the numbers numerically
const numberA = parseInt(matchA[2]);
const numberB = parseInt(matchB[2]);
return numberA - numberB;
} else {
// Handle cases where one or both strings do not match the expected pattern
// Simple strings are treated as pure alphabetical comparisons
return a.localeCompare(b);
// Extract the name and number from each string using regular expressions
const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex);
const matchB = b.match(regex);
if (matchA && matchB) {
// Compare the names alphabetically
const nameA = matchA[1];
const nameB = matchB[1];
if (nameA !== nameB) {
return nameA.localeCompare(nameB);
}
// If the names are the same, compare the numbers numerically
const numberA = parseInt(matchA[2]);
const numberB = parseInt(matchB[2]);
return numberA - numberB;
} else {
// Handle cases where one or both strings do not match the expected pattern
// Simple strings are treated as pure alphabetical comparisons
return a.localeCompare(b);
}
}

View file

@ -29,7 +29,7 @@ import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
import { classNames, cn } from "../../../../utils/utils";
import ToolbarSelectItem from "./toolbarSelectItem";
export default function NodeToolbarComponent({
@ -68,7 +68,7 @@ export default function NodeToolbarComponent({
const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false;
// const frozen = data.node?.frozen ?? false;
const frozen = data.node?.frozen ?? false;
const paste = useFlowStore((state) => state.paste);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -141,6 +141,9 @@ export default function NodeToolbarComponent({
break;
case "disabled":
break;
case "unselect":
unselectAll();
break;
case "ungroup":
takeSnapshot();
expandGroupNode(
@ -180,10 +183,10 @@ export default function NodeToolbarComponent({
case "update":
takeSnapshot();
// to update we must get the code from the templates in useTypesStore
const thisNodeTemplate = templates[data.type].template;
const thisNodeTemplate = templates[data.type]?.template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
if (!thisNodeTemplate?.code) return;
const currentCode = thisNodeTemplate.code.value;
if (data.node) {
@ -271,16 +274,20 @@ export default function NodeToolbarComponent({
selected &&
(hasApiKey || hasStore) &&
(event.ctrlKey || event.metaKey) &&
event.key === "u"
event.key.toUpperCase() === "U"
) {
event.preventDefault();
handleSelectChange("update");
}
if (selected && event.key.toUpperCase() === "ESCAPE") {
event.preventDefault();
handleSelectChange("unselect");
}
if (
selected &&
isGroup &&
(event.ctrlKey || event.metaKey) &&
event.key === "g"
event.key.toUpperCase() === "G"
) {
event.preventDefault();
handleSelectChange("ungroup");
@ -290,7 +297,7 @@ export default function NodeToolbarComponent({
(hasApiKey || hasStore) &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "S"
event.key.toUpperCase() === "S"
) {
event.preventDefault();
setShowconfirmShare((state) => !state);
@ -300,7 +307,7 @@ export default function NodeToolbarComponent({
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "Q"
event.key.toUpperCase() === "Q"
) {
event.preventDefault();
if (isMinimal) {
@ -317,7 +324,7 @@ export default function NodeToolbarComponent({
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "U"
event.key.toUpperCase() === "U"
) {
event.preventDefault();
if (hasCode) return setOpenModal((state) => !state);
@ -327,12 +334,16 @@ export default function NodeToolbarComponent({
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "A"
event.key.toUpperCase() === "A"
) {
event.preventDefault();
setShowModalAdvanced((state) => !state);
}
if (selected && (event.ctrlKey || event.metaKey) && event.key === "s") {
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "S"
) {
if (isSaved) {
event.preventDefault();
return setShowOverrideModal((state) => !state);
@ -347,7 +358,7 @@ export default function NodeToolbarComponent({
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "D"
event.key.toUpperCase() === "D"
) {
event.preventDefault();
if (data.node?.documentation) {
@ -357,7 +368,11 @@ export default function NodeToolbarComponent({
title: `${data.id} docs is not available at the moment.`,
});
}
if (selected && (event.ctrlKey || event.metaKey) && event.key === "j") {
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "J"
) {
event.preventDefault();
downloadNode(flowComponent!);
}
@ -372,7 +387,7 @@ export default function NodeToolbarComponent({
return (
<>
<div className="w-26 h-10">
<div className="w-26 nocopy nowheel nopan nodelete nodrag noundo h-10">
<span className="isolate inline-flex rounded-md shadow-sm">
{hasCode && (
<ShadTooltip content="Code" side="top">
@ -422,10 +437,10 @@ export default function NodeToolbarComponent({
</button>
</ShadTooltip>
{/* <ShadTooltip content="Freeze" side="top">
<ShadTooltip content="Freeze" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
)}
onClick={(event) => {
event.preventDefault();
@ -435,7 +450,7 @@ export default function NodeToolbarComponent({
...old.data,
node: {
...old.data.node,
// frozen: old.data?.node?.frozen ? false : true,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
@ -446,11 +461,11 @@ export default function NodeToolbarComponent({
className={cn(
"h-4 w-4 transition-all",
// TODO UPDATE THIS COLOR TO BE A VARIABLE
frozen ? "animate-wiggle text-ice" : ""
frozen ? "animate-wiggle text-ice" : "",
)}
/>
</button>
</ShadTooltip> */}
</ShadTooltip>
<Select onValueChange={handleSelectChange} value="">
<ShadTooltip content="More" side="top">
@ -483,16 +498,6 @@ export default function NodeToolbarComponent({
/>
</SelectItem>
)}
{/* <SelectItem value={"duplicate"}>
<ToolbarSelectItem
keyboardKey="D"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Duplicate"}
icon={"Copy"}
dataTestId="duplicate-button-modal"
/>
</SelectItem> */}
<SelectItem value={"copy"}>
<ToolbarSelectItem
keyboardKey="C"

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore";
@ -16,9 +16,19 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const { id } = useParams();
const navigate = useNavigate();
const flows = useFlowsManagerStore((state) => state.flows);
// Set flow tab id
useEffect(() => {
const isAnExistingFlow = flows.some((flow) => flow.id === id);
if (!isAnExistingFlow) {
navigate("/all");
return;
}
setCurrentFlowId(id!);
setOnFlowPage(true);

View file

@ -66,6 +66,7 @@ export default function ComponentsComponent({
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
const addFlow = useFlowsManagerStore((state) => state.addFlow);
useEffect(() => {
setFolderUrl(folderId ?? "");
@ -115,7 +116,7 @@ export default function ComponentsComponent({
});
};
const handleSelectOptionsChange = () => {
const handleSelectOptionsChange = (action: string) => {
const hasSelected = selectedFlowsComponentsCards?.length > 0;
if (!hasSelected) {
setErrorData({
@ -124,7 +125,31 @@ export default function ComponentsComponent({
});
return;
}
setOpenDelete(true);
if (action === "delete") {
setOpenDelete(true);
} else if (action === "duplicate") {
handleDuplicate();
}
};
const handleDuplicate = () => {
Promise.all(
selectedFlowsComponentsCards.map((selectedFlow) =>
addFlow(
true,
allFlows.find((flow) => flow.id === selectedFlow),
),
),
).then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSelectedFlowsComponentsCards([]);
setSuccessData({ title: "Flows duplicated successfully" });
});
};
const handleDeleteMultiple = () => {
@ -198,9 +223,10 @@ export default function ComponentsComponent({
<>
{allFlows?.length > 0 && (
<HeaderComponent
handleDelete={handleSelectOptionsChange}
handleDelete={() => handleSelectOptionsChange("delete")}
handleSelectAll={handleSelectAll}
disableDelete={!(selectedFlowsComponentsCards?.length > 0)}
handleDuplicate={() => handleSelectOptionsChange("duplicate")}
disableFunctions={!(selectedFlowsComponentsCards?.length > 0)}
/>
)}

View file

@ -1,5 +1,7 @@
import { useNavigate } from "react-router-dom";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import NewFlowModal from "../../../../modals/newFlowModal";
import { useState } from "react";
type EmptyComponentProps = {};
@ -7,31 +9,28 @@ const EmptyComponent = ({}: EmptyComponentProps) => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const [openModal, setOpenModal] = useState(false);
return (
<>
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4 text-muted-foreground">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1 ">
<span className="text-muted-foreground">New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
<div className="align-center flex w-full justify-center gap-1 ">
<span className="text-muted-foreground">
This folder is empty. New?
</span>
<span className="transition-colors hover:text-muted-foreground">
<NewFlowModal open={openModal} setOpen={setOpenModal} />
<button
onClick={() => {
setOpenModal(true);
}}
className="underline"
>
Start Here
</button>
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>

View file

@ -16,13 +16,15 @@ import ShadTooltip from "../../../../components/shadTooltipComponent";
type HeaderComponentProps = {
handleSelectAll: (select) => void;
handleDelete: () => void;
disableDelete: boolean;
handleDuplicate: () => void;
disableFunctions: boolean;
};
const HeaderComponent = ({
handleSelectAll,
handleDelete,
disableDelete,
handleDuplicate,
disableFunctions,
}: HeaderComponentProps) => {
const [shouldSelectAll, setShouldSelectAll] = useState(true);
@ -55,23 +57,41 @@ const HeaderComponent = ({
</div>
</a>
</div>
<div className="col-span-2 grid-cols-1 justify-self-end">
<div className="col-span-2 flex grid-cols-1 gap-2 justify-self-end">
<div>
<ShadTooltip
content={
disableDelete ? (
disableFunctions ? (
<span>Select items to duplicate</span>
) : (
<span>Duplicate selected items</span>
)
}
>
<button onClick={handleDuplicate} disabled={disableFunctions}>
<IconComponent
name="Copy"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</button>
</ShadTooltip>
</div>
<div>
<ShadTooltip
content={
disableFunctions ? (
<span>Select items to delete</span>
) : (
<span>Delete selected items</span>
)
}
>
<button onClick={handleDelete} disabled={disableDelete}>
<button onClick={handleDelete} disabled={disableFunctions}>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
disableDelete ? "" : "hover:text-destructive",
disableFunctions ? "" : "hover:text-destructive",
)}
/>
</button>

View file

@ -6,13 +6,14 @@ export type FolderType = {
id?: string | null;
parent_id: string;
flows: FlowType[];
components: string[];
};
export type AddFolderType = {
name: string;
description: string;
id?: string | null;
parent_id: string;
parent_id: string | null;
flows?: string[];
components?: string[];
};

View file

@ -1,7 +1,7 @@
import { BASE_URL_API } from "../../../constants/constants";
import { api } from "../../../controllers/API/api";
import { FlowType } from "../../../types/flow";
import { AddFolderType, FolderType, StarterProjectsType } from "../entities";
import { AddFolderType, FolderType } from "../entities";
export async function getFolders(): Promise<FolderType[]> {
try {
@ -30,12 +30,12 @@ export async function addFolder(data: AddFolderType): Promise<FolderType> {
export async function updateFolder(
body: FolderType,
folderId: string,
folderId: string
): Promise<FolderType> {
try {
const response = await api.patch(
`${BASE_URL_API}folders/${folderId}`,
body,
body
);
return response?.data;
} catch (error) {
@ -61,15 +61,6 @@ export async function getFolderById(folderId: string): Promise<FolderType> {
}
}
export async function getStarterProjects(): Promise<StarterProjectsType> {
try {
const response = await api.get(`${BASE_URL_API}folders/starter-projects`);
return response?.data;
} catch (error) {
throw error;
}
}
export async function downloadFlowsFromFolders(folderId: string): Promise<{
flows: FlowType[];
folder_name: string;
@ -77,7 +68,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{
}> {
try {
const response = await api.get(
`${BASE_URL_API}folders/download/${folderId}`,
`${BASE_URL_API}folders/download/${folderId}`
);
if (response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
@ -91,7 +82,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{
}
export async function uploadFlowsFromFolders(
flows: FormData,
flows: FormData
): Promise<FlowType[]> {
try {
const response = await api.post(`${BASE_URL_API}folders/upload/`, flows);
@ -108,11 +99,11 @@ export async function uploadFlowsFromFolders(
export async function moveFlowToFolder(
flowId: string,
folderId: string,
folderId: string
): Promise<FlowType> {
try {
const response = await api.patch(
`${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`,
`${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`
);
return response?.data;
} catch (error) {

View file

@ -29,7 +29,6 @@ export default function PlaygroundPage() {
// Set flow tab id
useEffect(() => {
console.log("id", id);
if (getFlowById(id!)) {
setCurrentFlowId(id!);
} else {

View file

@ -21,7 +21,7 @@ export default function SettingsPage(): JSX.Element {
icon: (
<ForwardedIconComponent
name="SlidersHorizontal"
className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]"
className="w-4 flex-shrink-0 justify-start stroke-[1.5]"
/>
),
},
@ -32,13 +32,23 @@ export default function SettingsPage(): JSX.Element {
icon: (
<ForwardedIconComponent
name="Globe"
className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]"
className="w-4 flex-shrink-0 justify-start stroke-[1.5]"
/>
),
},
{
title: "Shortcuts",
href: "/settings/shortcuts",
icon: (
<ForwardedIconComponent
name="Keyboard"
className="w-4 flex-shrink-0 justify-start stroke-[1.5]"
/>
),
},
{
title: "Messages",
href: "/settings/messages",
icon: (
<ForwardedIconComponent name="Keyboard" className="w-5 stroke-[1.5]" />
),
@ -50,10 +60,10 @@ export default function SettingsPage(): JSX.Element {
description="Manage the general settings for Langflow."
>
<div className="flex h-full w-full space-y-8 lg:flex-row lg:space-x-8 lg:space-y-0">
<aside className="flex h-full flex-col space-y-6 lg:w-1/5">
<aside className="flex h-full shrink-0 flex-col space-y-6 lg:w-[20vw]">
<SidebarNav items={sidebarNavItems} />
</aside>
<div className="h-full w-full flex-1">
<div className="h-full w-full flex-1 pb-8">
<Outlet />
</div>
</div>

View file

@ -0,0 +1,23 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
const GeneralPageHeaderComponent = () => {
return (
<>
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
General
<ForwardedIconComponent
name="SlidersHorizontal"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage settings related to Langflow and your account.
</p>
</div>
</div>
</>
);
};
export default GeneralPageHeaderComponent;

View file

@ -0,0 +1,93 @@
import * as Form from "@radix-ui/react-form";
import InputComponent from "../../../../../../components/inputComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
type PasswordFormComponentProps = {
password: string;
cnfPassword: string;
handleInput: (event: any) => void;
handlePatchPassword: (
password: string,
cnfPassword: string,
handleInput: any,
) => void;
};
const PasswordFormComponent = ({
password,
cnfPassword,
handleInput,
handlePatchPassword,
}: PasswordFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
handlePatchPassword(password, cnfPassword, handleInput);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message match="valueMissing" className="field-invalid">
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<Form.Message className="field-invalid" match="valueMissing">
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default PasswordFormComponent;

View file

@ -0,0 +1,68 @@
import * as Form from "@radix-ui/react-form";
import GradientChooserComponent from "../../../../../../components/gradientChooserComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
import { gradients } from "../../../../../../utils/styleUtils";
type ProfileGradientFormComponentProps = {
gradient: string;
handleInput: (event: any) => void;
handlePatchGradient: (gradient: string) => void;
userData: any;
};
const ProfileGradientFormComponent = ({
gradient,
handleInput,
handlePatchGradient,
userData,
}: ProfileGradientFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
handlePatchGradient(gradient);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: gradient
}
onChange={(value) => {
handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default ProfileGradientFormComponent;

View file

@ -0,0 +1,102 @@
import * as Form from "@radix-ui/react-form";
import InputComponent from "../../../../../../components/inputComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
import {
CREATE_API_KEY,
INSERT_API_KEY,
INVALID_API_KEY,
NO_API_KEY,
} from "../../../../../../constants/constants";
type StoreApiKeyFormComponentProps = {
apikey: string;
handleInput: (event: any) => void;
handleSaveKey: (apikey: string, handleInput: any) => void;
loadingApiKey: boolean;
validApiKey: boolean;
hasApiKey: boolean;
};
const StoreApiKeyFormComponent = ({
apikey,
handleInput,
handleSaveKey,
loadingApiKey,
validApiKey,
hasApiKey,
}: StoreApiKeyFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
handleSaveKey(apikey, handleInput);
}}
>
<Card x-chunk="dashboard-04-chunk-2" id="api">
<CardHeader>
<CardTitle>Store API Key</CardTitle>
<CardDescription>
{(hasApiKey && !validApiKey
? INVALID_API_KEY
: !hasApiKey
? NO_API_KEY
: "") + INSERT_API_KEY}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full flex-col gap-3">
<div className="flex w-full gap-4">
<Form.Field name="apikey" className="w-full">
<InputComponent
id="apikey"
onChange={(value) => {
handleInput({ target: { name: "apikey", value } });
}}
value={apikey}
isForm
password={true}
placeholder="Insert your API Key"
className="w-full"
/>
<Form.Message match="valueMissing" className="field-invalid">
Please enter your API Key
</Form.Message>
</Form.Field>
</div>
<span className="pr-1 text-xs text-muted-foreground">
{CREATE_API_KEY}{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button
loading={loadingApiKey}
type="submit"
data-testid="api-key-save-button-store"
>
Save
</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default StoreApiKeyFormComponent;

View file

@ -1,221 +1,107 @@
import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import GradientChooserComponent from "../../../../components/gradientChooserComponent";
import InputComponent from "../../../../components/inputComponent";
import { Button } from "../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../components/ui/card";
import {
EDIT_PASSWORD_ALERT_LIST,
EDIT_PASSWORD_ERROR_ALERT,
SAVE_ERROR_ALERT,
SAVE_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants";
import { useContext, useState } from "react";
import { useParams } from "react-router-dom";
import { CONTROL_PATCH_USER_STATE } from "../../../../constants/constants";
import { AuthContext } from "../../../../contexts/authContext";
import { resetPassword, updateUser } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../../stores/storeStore";
import {
inputHandlerEventType,
patchUserInputStateType,
} from "../../../../types/components";
import { gradients } from "../../../../utils/styleUtils";
import usePatchGradient from "../hooks/use-patch-gradient";
import usePatchPassword from "../hooks/use-patch-password";
import useSaveKey from "../hooks/use-save-key";
import useScrollToElement from "../hooks/use-scroll-to-element";
import GeneralPageHeaderComponent from "./components/GeneralPageHeader";
import PasswordFormComponent from "./components/PasswordForm";
import ProfileGradientFormComponent from "./components/ProfileGradientForm";
import StoreApiKeyFormComponent from "./components/StoreApiKeyForm";
export default function GeneralPage() {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const { scrollId } = useParams();
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE,
);
const { autoLogin } = useContext(AuthContext);
// set null id
useEffect(() => {
setCurrentFlowId("");
}, []);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { userData, setUserData } = useContext(AuthContext);
const { password, cnfPassword, gradient } = inputState;
const hasStore = useStoreStore((state) => state.hasStore);
async function handlePatchPassword() {
if (password !== cnfPassword) {
setErrorData({
title: EDIT_PASSWORD_ERROR_ALERT,
list: [EDIT_PASSWORD_ALERT_LIST],
});
return;
}
try {
if (password !== "") await resetPassword(userData!.id, { password });
handleInput({ target: { name: "password", value: "" } });
handleInput({ target: { name: "cnfPassword", value: "" } });
setSuccessData({ title: SAVE_SUCCESS_ALERT });
} catch (error) {
setErrorData({
title: SAVE_ERROR_ALERT,
list: [(error as any).response.data.detail],
});
}
}
const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const setHasApiKey = useStoreStore((state) => state.updateHasApiKey);
const loadingApiKey = useStoreStore((state) => state.loadingApiKey);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const setLoadingApiKey = useStoreStore((state) => state.updateLoadingApiKey);
const { password, cnfPassword, gradient, apikey } = inputState;
async function handlePatchGradient() {
try {
if (gradient !== "")
await updateUser(userData!.id, { profile_image: gradient });
if (gradient !== "") {
let newUserData = cloneDeep(userData);
newUserData!.profile_image = gradient;
const { handlePatchPassword } = usePatchPassword(
userData,
setSuccessData,
setErrorData,
);
setUserData(newUserData);
}
setSuccessData({ title: SAVE_SUCCESS_ALERT });
} catch (error) {
setErrorData({
title: SAVE_ERROR_ALERT,
list: [(error as any).response.data.detail],
});
}
}
const { handlePatchGradient } = usePatchGradient(
setSuccessData,
setErrorData,
userData,
setUserData,
);
useScrollToElement(scrollId, setCurrentFlowId);
const { handleSaveKey } = useSaveKey(
setSuccessData,
setErrorData,
setHasApiKey,
setValidApiKey,
setLoadingApiKey,
);
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
return (
<div className="flex h-full w-full flex-col gap-6">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
General
<ForwardedIconComponent
name="SlidersHorizontal"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage settings related to Langflow and your account.
</p>
</div>
</div>
<GeneralPageHeaderComponent />
<div className="grid gap-6">
<Form.Root
onSubmit={(event) => {
handlePatchGradient();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: gradient
}
onChange={(value) => {
handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
{!autoLogin && (
<Form.Root
onSubmit={(event) => {
handlePatchPassword();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message
match="valueMissing"
className="field-invalid"
>
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<ProfileGradientFormComponent
gradient={gradient}
handleInput={handleInput}
handlePatchGradient={handlePatchGradient}
userData={userData}
/>
<Form.Message
className="field-invalid"
match="valueMissing"
>
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
{!autoLogin && (
<PasswordFormComponent
password={password}
cnfPassword={cnfPassword}
handleInput={handleInput}
handlePatchPassword={handlePatchPassword}
/>
)}
{hasStore && (
<StoreApiKeyFormComponent
apikey={apikey}
handleInput={handleInput}
handleSaveKey={handleSaveKey}
loadingApiKey={loadingApiKey}
validApiKey={validApiKey}
hasApiKey={hasApiKey}
/>
)}
</div>
</div>

View file

@ -78,9 +78,6 @@ export default function GlobalVariablesPage() {
// Column Definitions: Defines the columns to be displayed.
const [colDefs, setColDefs] = useState<(ColDef<any> | ColGroupDef<any>)[]>([
{
headerCheckboxSelection: true,
checkboxSelection: true,
showDisabledCheckboxes: true,
headerName: "Variable Name",
field: "name",
flex: 2,
@ -170,7 +167,7 @@ export default function GlobalVariablesPage() {
</div>
</div>
<div className="flex h-full w-full flex-col justify-between pb-8">
<div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
<CardContent className="h-full">
<TableComponent

View file

@ -114,7 +114,7 @@ export default function ShortcutsPage() {
</p>
</div>
</div>
<div className="grid gap-6 pb-8">
<div className="grid gap-6">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
<CardContent className="h-full">
<TableComponent

View file

@ -0,0 +1,37 @@
import cloneDeep from "lodash/cloneDeep";
import {
SAVE_ERROR_ALERT,
SAVE_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants";
import { updateUser } from "../../../../controllers/API";
const usePatchGradient = (
setSuccessData,
setErrorData,
currentUserData,
setUserData,
) => {
const handlePatchGradient = async (gradient) => {
try {
if (gradient !== "") {
await updateUser(currentUserData.id, { profile_image: gradient });
let newUserData = cloneDeep(currentUserData);
newUserData.profile_image = gradient;
setUserData(newUserData);
}
setSuccessData({ title: SAVE_SUCCESS_ALERT });
} catch (error) {
setErrorData({
title: SAVE_ERROR_ALERT,
list: [(error as any)?.response?.data?.detail],
});
}
};
return {
currentUserData,
handlePatchGradient,
};
};
export default usePatchGradient;

View file

@ -0,0 +1,36 @@
import {
EDIT_PASSWORD_ALERT_LIST,
EDIT_PASSWORD_ERROR_ALERT,
SAVE_ERROR_ALERT,
SAVE_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants";
import { resetPassword } from "../../../../controllers/API";
const usePatchPassword = (userData, setSuccessData, setErrorData) => {
const handlePatchPassword = async (password, cnfPassword, handleInput) => {
if (password !== cnfPassword) {
setErrorData({
title: EDIT_PASSWORD_ERROR_ALERT,
list: [EDIT_PASSWORD_ALERT_LIST],
});
return;
}
try {
if (password !== "") await resetPassword(userData.id, { password });
handleInput({ target: { name: "password", value: "" } });
handleInput({ target: { name: "cnfPassword", value: "" } });
setSuccessData({ title: SAVE_SUCCESS_ALERT });
} catch (error) {
setErrorData({
title: SAVE_ERROR_ALERT,
list: [(error as any)?.response?.data?.detail],
});
}
};
return {
handlePatchPassword,
};
};
export default usePatchPassword;

Some files were not shown because too many files have changed in this diff Show more