Merge branch 'dev' into cz/inspection

This commit is contained in:
anovazzi1 2024-06-05 20:07:11 -03:00
commit e2c1f2f027
179 changed files with 7657 additions and 6695 deletions

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

@ -222,12 +222,19 @@ export default function App() {
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
alert.type === "notice" && (
<NoticeAlert
alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
@ -236,20 +243,6 @@ export default function App() {
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
</div>
</div>
);

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";
@ -70,7 +69,12 @@ export default function AddNewVariableButton({ children }): JSX.Element {
});
}
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,11 +141,9 @@ export default function AddNewVariableButton({ children }): JSX.Element {
></InputComponent>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button data-testid="save-variable-button" onClick={handleSaveVariable}>
Save Variable
</Button>
</BaseModal.Footer>
<BaseModal.Footer
submit={{ label: "Save Variable", dataTestId: "save-variable-btn" }}
/>
</BaseModal>
);
}

View file

@ -29,7 +29,7 @@ export default function DictComponent({
<div
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
"flex flex-col gap-3",
)}
>
{

View file

@ -99,7 +99,7 @@ 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}
@ -109,7 +109,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
{setEndpointName && (
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium">Endpoint name:</span>
<span className="font-medium">Endpoint Name</span>
{!isEndpointNameValid && (
<span className="edit-flow-span">
Invalid endpoint name. Use only letters, numbers, hyphens, and
@ -123,7 +123,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name for the run endpoint"
placeholder="An alternative name to run the endpoint"
maxLength={maxLength}
id="endpoint_name"
onDoubleClickCapture={(event) => {

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

@ -35,21 +35,11 @@ export const MenuBar = ({}: {}): JSX.Element => {
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> });
}
@ -89,15 +79,6 @@ export const MenuBar = ({}: {}): JSX.Element => {
<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={() => {
@ -132,7 +113,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}
},
);
}}
>
@ -214,7 +195,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
)}
/>
{printByBuildStatus()}

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

@ -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

@ -1,6 +1,7 @@
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 { extractColumnsFromRows } from "../../utils/utils";
import TableComponent from "../tableComponent";

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: {
@ -11,9 +12,12 @@ type SideBarButtonsComponentProps = {
pathname: string;
handleOpenNewFolderModal?: () => void;
};
const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
const SideBarButtonsComponent = ({
items,
pathname,
}: SideBarButtonsComponentProps) => {
return (
<>
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
{items.map((item) => (
<Link to={item.href!}>
<div
@ -21,14 +25,20 @@ const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
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",
)}
>
{item.title}
{item.icon}
<span className="block max-w-full truncate opacity-100">
{item.title}
</span>
</div>
</Link>
))}
</>
</div>
);
};
export default SideBarButtonsComponent;

View file

@ -33,7 +33,7 @@ const SideBarFoldersButtonsComponent = ({
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false }))
folders.map((obj) => ({ name: obj.name, edit: false })),
);
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
@ -58,7 +58,7 @@ const SideBarFoldersButtonsComponent = ({
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId,
handleFolderChange
handleFolderChange,
);
const handleUploadFlowsToFolder = () => {
@ -73,7 +73,7 @@ const SideBarFoldersButtonsComponent = ({
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
}
},
);
}
@ -93,24 +93,25 @@ const SideBarFoldersButtonsComponent = ({
return (
<>
<div className="flex shrink-0 items-center justify-between">
<Button variant="primary" onClick={addNewFolder}>
<ForwardedIconComponent
name="Plus"
className="main-page-nav-button"
/>
New Folder
<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"
size="icon"
className="px-2"
onClick={addNewFolder}
data-testid="add-folder-button"
>
<ForwardedIconComponent name="FolderPlus" className="w-4" />
</Button>
<Button
variant="primary"
className="px-7"
size="icon"
className="px-2"
onClick={handleUploadFlowsToFolder}
data-testid="upload-folder-button"
>
<ForwardedIconComponent
name="Upload"
className="main-page-nav-button"
/>
Upload
<ForwardedIconComponent name="Upload" className="w-4" />
</Button>
</div>
@ -118,7 +119,7 @@ const SideBarFoldersButtonsComponent = ({
<>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name
(folder) => folder.name === item.name,
)[0];
return (
<div
@ -134,7 +135,7 @@ const SideBarFoldersButtonsComponent = ({
? "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" : ""
folderIdDragging === item.id! ? "bg-border" : "",
)}
onClick={() => handleChangeFolder!(item.id!)}
>
@ -176,11 +177,11 @@ const SideBarFoldersButtonsComponent = ({
event.stopPropagation();
event.preventDefault();
}}
className="flex w-full items-center gap-2"
className="flex w-full items-center gap-4"
>
<IconComponent
name={"folder"}
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
className="w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
{editFolderName?.edit ? (
<div>
@ -204,7 +205,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
}))
})),
);
}
if (e.key === "Enter") {
@ -237,10 +238,10 @@ const SideBarFoldersButtonsComponent = ({
};
const updatedFolder = await updateFolder(
body,
item.id!
item.id!,
);
const updateFolders = folders.filter(
(f) => f.name !== item.name
(f) => f.name !== item.name,
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
@ -248,7 +249,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
}))
})),
);
} else {
setFoldersNames((old) => ({
@ -263,11 +264,10 @@ const SideBarFoldersButtonsComponent = ({
/>
</div>
) : (
<span className="block max-w-full truncate opacity-100">
<span className="block 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]"
@ -284,21 +284,6 @@ const SideBarFoldersButtonsComponent = ({
/>
</Button>
)}
{/* {index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"pencil"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
)} */}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
@ -306,7 +291,8 @@ const SideBarFoldersButtonsComponent = ({
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
size="none"
variant="none"
>
<IconComponent
name={"Download"}

View file

@ -41,16 +41,20 @@ export default function SidebarNav({
return (
<nav className={cn(className)} {...props}>
<HorizontalScrollFadeComponent>
<SideBarButtonsComponent items={items} pathname={pathname} />
{!loadingFolders && folders?.length > 0 && isFolderPath && (
<SideBarFoldersButtonsComponent
folders={folders}
pathname={pathname}
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
/>
{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

@ -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 } 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,7 +36,67 @@ 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);
const initialColumnDefs = useRef(colDef);
const makeLastColumnNonResizable = (columnDefs) => {
columnDefs.forEach((colDef, index) => {
colDef.resizable = index !== columnDefs.length - 1;
});
return columnDefs;
};
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 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 (
<div className="flex h-full w-full items-center justify-center rounded-md border">
@ -46,12 +111,12 @@ 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
@ -62,8 +127,13 @@ const TableComponent = forwardRef<
autoHeight: true,
}}
tooltipInteraction={true}
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

@ -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 = {
@ -738,3 +739,5 @@ export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to displa
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];
export const MAX_BATCH_SIZE = 50;

View file

@ -2,11 +2,11 @@ import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { renewAccessToken } from ".";
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../constants/constants";
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({
@ -81,29 +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 lastMethodCalled = localStorage.getItem("lastMethodCalled");
const checkRequest = checkDuplicateRequestAndStoreRequest(config);
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) =>
config?.url!.includes(request),
);
if (
config?.url === lastUrl &&
!isContained &&
lastMethodCalled === config.method &&
lastMethodCalled === "get"
) {
return Promise.reject("Duplicate request");
if (!checkRequest) {
return Promise.reject("Duplicate request.");
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem(
"lastRequestData",
JSON.stringify(config.data) ?? "",
);
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.
@ -61,7 +62,7 @@ export async function sendAll(data: sendAllProps) {
}
export async function postValidateCode(
code: string
code: string,
): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code });
}
@ -76,7 +77,7 @@ export async function postValidateCode(
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType
frontend_node: APIClassType,
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
@ -149,7 +150,7 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType
updatedFlow: FlowType,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
@ -327,7 +328,7 @@ export async function getHealth() {
*
*/
export async function getBuildStatus(
flowId: string
flowId: string,
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
@ -340,7 +341,7 @@ export async function getBuildStatus(
*
*/
export async function postBuildInit(
flow: FlowType
flow: FlowType,
): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
@ -356,7 +357,7 @@ export async function postBuildInit(
*/
export async function uploadFile(
file: File,
id: string
id: string,
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
@ -365,7 +366,7 @@ export async function uploadFile(
export async function postCustomComponent(
code: string,
apiClass: APIClassType
apiClass: APIClassType,
): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
@ -378,7 +379,7 @@ export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any
field_value: any,
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
@ -400,7 +401,7 @@ export async function onLogin(user: LoginType) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
},
);
if (response.status === 200) {
@ -462,11 +463,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage(
skip: number,
limit: number
limit: number,
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
);
if (res.status === 200) {
return res.data;
@ -503,7 +504,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user
user,
);
if (res.status === 200) {
return res.data;
@ -577,7 +578,7 @@ export async function saveFlowStore(
last_tested_version?: string;
},
tags: string[],
publicFlow = false
publicFlow = false,
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -706,7 +707,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
`${BASE_URL_API}store/components/${component_id}`,
);
if (res.status === 200) {
return res.data;
@ -721,7 +722,7 @@ export async function searchComponent(
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
tags?: string[],
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
@ -833,7 +834,7 @@ export async function updateFlowStore(
},
tags: string[],
publicFlow = false,
id: string
id: string,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -917,7 +918,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable(
name: string,
value: string,
id: string
id: string,
) {
try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -936,7 +937,7 @@ export async function getVerticesOrder(
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?: Node[],
Edges?: Edge[]
Edges?: Edge[],
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
@ -956,7 +957,7 @@ export async function getVerticesOrder(
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`,
data,
config
config,
);
}
@ -964,7 +965,7 @@ export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string,
files?: string[]
files?: string[],
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
const data = input_value
@ -975,7 +976,7 @@ export async function postBuildVertex(
}
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
data
data,
);
}
@ -999,25 +1000,54 @@ export async function getFlowPool({
}
export async function deleteFlowPool(
flowId: string
flowId: string,
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
}
/**
* 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,
});
flowIds: string[],
): 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(
id: string,
mode: "intersection" | "union",
params = {}
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
@ -1030,16 +1060,47 @@ export async function getTransactionTable(
}
export async function getMessagesTable(
id: string,
mode: "intersection" | "union",
params = {}
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
id?: string,
excludedFields?: string[],
params = {},
): 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

@ -54,10 +54,10 @@ export default function GenericNode({
const setErrorData = useAlertStore((state) => state.setErrorData);
const isDark = useDarkStore((state) => state.dark);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
(state) => state.flowBuildStatus[data.id]?.status,
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
(state) => state.flowBuildStatus[data.id]?.timestamp,
);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
@ -65,7 +65,7 @@ export default function GenericNode({
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
data.node?.description!,
);
const [isOutdated, setIsOutdated] = useState(false);
const [validationStatus, setValidationStatus] =
@ -83,7 +83,7 @@ export default function GenericNode({
data.node!,
setNode,
setIsOutdated,
updateNodeInternals
updateNodeInternals,
);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
@ -114,12 +114,12 @@ export default function GenericNode({
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null
validationStatus: VertexBuildTypeAPI | null,
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus,
isDark
isDark,
);
const baseBorderClass = getBaseBorderClass(selected);
@ -128,7 +128,7 @@ export default function GenericNode({
baseBorderClass,
nodeSizeClass,
"generic-node-div",
specificClassFromBuildStatus
specificClassFromBuildStatus,
);
return names;
};
@ -170,7 +170,7 @@ export default function GenericNode({
showNode,
isEmoji,
nodeIconFragment,
checkNodeIconFragment
checkNodeIconFragment,
);
function countHandles(): void {
@ -247,7 +247,7 @@ export default function GenericNode({
selected,
showNode,
buildStatus,
validationStatus
validationStatus,
)}
>
{data.node?.beta && showNode && (
@ -270,6 +270,7 @@ export default function GenericNode({
"generic-node-title-arrangement rounded-full" +
(!showNode && " justify-center ")
}
data-testid="generic-node-title-arrangement"
>
{iconNodeRender()}
{showNode && (
@ -306,7 +307,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);
}
@ -320,21 +321,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>
@ -392,7 +378,7 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
@ -420,7 +406,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
),
)}
<ParameterComponent
key={scapedJSONStringfy({
@ -561,14 +547,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();
}}
@ -628,13 +614,13 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n"
"\n",
) ?? data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
@ -661,7 +647,7 @@ export default function GenericNode({
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center"
"flex-max-width justify-center",
)}
>
{" "}

View file

@ -9,7 +9,7 @@ const useFetchDataOnMount = (
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -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,7 @@ const useHandleOnNewValue = (
debouncedHandleUpdateValues,
setNode,
renderTooltips,
setIsLoading
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -44,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,5 +1,11 @@
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select";
import {
CHAT_FIRST_INITIAL_TEXT,
CHAT_SECOND_INITIAL_TEXT,
@ -61,7 +67,7 @@ export default function ChatView({
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
//
.filter(
(output) => output.data.messages && output.data.messages.length > 0
(output) => output.data.messages && output.data.messages.length > 0,
)
.map((output, index) => {
try {
@ -116,10 +122,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;
@ -147,18 +164,57 @@ export default function ChatView({
<div className="eraser-column-arrangement">
<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"
"h-5 w-5 transition-all duration-100",
lockChat ? "animate-pulse text-primary" : "text-primary",
)}
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,
@ -25,6 +24,7 @@ import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView";
import ChatView from "./components/chatView";
import { getSessions } from "../../controllers/API";
export default function IOModal({
children,
@ -78,6 +78,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 +90,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 +105,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 +128,11 @@ export default function IOModal({
useEffect(() => {
setSelectedViewField(startView());
// if (haveChat) {
// getSessions().then((sessions) => {
// setSessions(sessions);
// });
// }
}, [open]);
return (
@ -128,6 +141,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 */}
@ -167,6 +181,9 @@ export default function IOModal({
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
{/* {haveChat && (
<TabsTrigger value={"3"}>History</TabsTrigger>
)} */}
</TabsList>
</div>
@ -260,6 +277,10 @@ export default function IOModal({
key={index}
>
<AccordionComponent
disabled={
node.data.node!.template["input_value"]
?.value === ""
}
trigger={
<div className="file-component-badge-div">
<ShadTooltip
@ -378,13 +399,10 @@ 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(
@ -394,10 +412,9 @@ export default function IOModal({
: "fill-current text-medium-indigo",
)}
/>
Run Flow
</Button>
</div>
</BaseModal.Footer>
),
}}
/>
) : (
<></>
)}

View file

@ -8,14 +8,14 @@ export function getCurlRunCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
endpointName?: string
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" \\
endpointName || flowId
}?stream=false" \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}

View file

@ -9,19 +9,35 @@ export default function getPythonApiCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
endpointName?: string,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
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,
flow_id: str,
endpoint: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
@ -30,11 +46,11 @@ def run_flow(message: str,
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 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}/{flow_id}"
api_url = f"{BASE_API_URL}/{endpoint}"
payload = {
"input_value": message,
@ -49,10 +65,43 @@ def run_flow(message: str,
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` : ""
}))`;
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

@ -1,7 +1,7 @@
export function createTabsArray(
codes,
includeWebhookCurl = false,
includeTweaks = false
includeTweaks = false,
) {
const tabs = [
{

View file

@ -35,7 +35,7 @@ const ApiModal = forwardRef(
flow: FlowType;
children: ReactNode;
},
ref
ref,
) => {
const tweak = useTweaksStore((state) => state.tweak);
const addTweaks = useTweaksStore((state) => state.setTweak);
@ -46,17 +46,22 @@ 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 pythonApiCode = getPythonApiCode(
flow?.id,
autoLogin,
tweak,
flow?.endpoint_name,
);
const curl_run_code = getCurlRunCode(
flow?.id,
autoLogin,
tweak,
flow?.endpoint_name
flow?.endpoint_name,
);
const curl_webhook_code = getCurlWebhookCode(
flow?.id,
autoLogin,
flow?.endpoint_name
flow?.endpoint_name,
);
const pythonCode = getPythonCode(flow?.name, tweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
@ -72,7 +77,7 @@ const ApiModal = forwardRef(
pythonCode,
];
const [tabs, setTabs] = useState(
createTabsArray(codesArray, includeWebhook)
createTabsArray(codesArray, includeWebhook),
);
const canShowTweaks =
@ -121,7 +126,7 @@ const ApiModal = forwardRef(
buildTweakObject(
nodeId,
element.data.node.template[templateField].value,
element.data.node.template[templateField]
element.data.node.template[templateField],
);
}
});
@ -138,7 +143,7 @@ const ApiModal = forwardRef(
async function buildTweakObject(
tw: string,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType
template: TemplateVariableType,
) {
changes = getChangesType(changes, template);
@ -180,7 +185,7 @@ const ApiModal = forwardRef(
flow?.id,
autoLogin,
cloneTweak,
flow?.endpoint_name
flow?.endpoint_name,
);
const pythonCode = getPythonCode(flow?.name, cloneTweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
@ -224,7 +229,7 @@ const ApiModal = forwardRef(
</BaseModal.Content>
</BaseModal>
);
}
},
);
export default ApiModal;

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: [
@ -92,6 +125,7 @@ interface BaseModalProps {
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
type?: "modal" | "dialog";
onSubmit?: () => void;
}
function BaseModal({
open,
@ -100,6 +134,7 @@ function BaseModal({
size = "large",
onChangeOpenModal,
type = "dialog",
onSubmit,
}: BaseModalProps) {
const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header,
@ -114,77 +149,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 max-h-[70vh]";
break;
case "medium-log":
minWidth = "min-w-[60vw]";
height = "h-[30vh]";
break;
default:
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
}
let { minWidth, height } = switchCaseModalSize(size);
useEffect(() => {
if (onChangeOpenModal) {
@ -219,13 +184,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

@ -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,
@ -53,7 +52,7 @@ const EditNodeModal = forwardRef(
setOpen: (open: boolean) => void;
data: NodeDataType;
},
ref
ref,
) => {
const nodes = useFlowStore((state) => state.nodes);
@ -102,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>
<></>
@ -125,7 +134,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: ""
: "",
)}
>
{nodeLength > 0 && (
@ -134,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>
@ -147,8 +157,8 @@ const EditNodeModal = forwardRef(
templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node!.template[templateParam].type
)
myData.node!.template[templateParam].type,
),
)
.map((templateParam, index) => {
let id = {
@ -170,8 +180,8 @@ const EditNodeModal = forwardRef(
myData.node?.template[templateParam]
.proxy,
}
: id
)
: id,
),
) ?? false;
return (
<TableRow
@ -188,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>
@ -205,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={
@ -233,7 +263,7 @@ const EditNodeModal = forwardRef(
onChange={(value: string[]) => {
handleOnNewValue(
value,
templateParam
templateParam,
);
}}
/>
@ -257,11 +287,11 @@ const EditNodeModal = forwardRef(
.value ?? ""
}
onChange={(
value: string | string[]
value: string | string[],
) => {
handleOnNewValue(
value,
templateParam
templateParam,
);
}}
/>
@ -311,7 +341,7 @@ const EditNodeModal = forwardRef(
].value = newValue;
handleOnNewValue(
newValue,
templateParam
templateParam,
);
}}
id="editnode-div-dict-input"
@ -328,7 +358,7 @@ const EditNodeModal = forwardRef(
myData.node!.template[templateParam].value
?.length > 1
? "my-3"
: ""
: "",
)}
>
<KeypairListComponent
@ -344,7 +374,7 @@ const EditNodeModal = forwardRef(
myData.node!.template[
templateParam
].value,
type(templateParam)!
type(templateParam)!,
)
}
duplicateKey={errorDuplicateKey}
@ -355,11 +385,11 @@ const EditNodeModal = forwardRef(
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
hasDuplicateKeys(valueToNumbers),
);
handleOnNewValue(
valueToNumbers,
templateParam
templateParam,
);
}}
isList={
@ -389,7 +419,7 @@ const EditNodeModal = forwardRef(
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
templateParam,
);
}}
size="small"
@ -610,29 +640,10 @@ 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>
);
}
},
);
export default EditNodeModal;

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

@ -16,41 +16,15 @@ export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow);
const nodes = useFlowStore((state) => state.nodes);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const flows = useFlowsManagerStore((state) => state.flows);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
useEffect(() => {
setName(currentFlow!.name);
setDescription(currentFlow!.description);
}, [currentFlow!.name, currentFlow!.description, open]);
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");
const noticed = useRef(false);
function handleClick(): void {
currentFlow!.name = name;
currentFlow!.description = description;
saveFlow(currentFlow!)
?.then(() => {
setOpen(false);
})
.catch((err) => {
useAlertStore.getState().setErrorData({
title: "Error while saving changes",
list: [(err as AxiosError).response?.data.detail ?? ""],
});
console.error(err);
});
}
useEffect(() => {
if (activeTab === "Executions") {
getTransactionTable(currentFlowId, "union").then((data) => {
@ -59,11 +33,13 @@ 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) {
@ -72,7 +48,7 @@ export default function FlowLogsModal({
.some((template) => template["stream"] && template["stream"].value);
console.log(
haStream,
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template)
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template),
);
if (haStream) {
setNoticeData({
@ -86,16 +62,6 @@ export default function FlowLogsModal({
}
}, [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,7 +1,6 @@
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";
@ -24,19 +23,21 @@ 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;
currentFlow!.endpoint_name = endpoint_name;
saveFlow(currentFlow!)
?.then(() => {
setOpen(false);
setIsSaving(false);
})
.catch((err) => {
useAlertStore.getState().setErrorData({
title: "Error while saving changes",
list: [(err as AxiosError).response?.data.detail ?? ""],
list: [err?.response?.data.detail ?? ""],
});
console.error(err);
});
@ -53,7 +54,12 @@ export default function FlowSettingsModal({
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="smaller-h-full">
<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 " />
@ -70,16 +76,14 @@ export default function FlowSettingsModal({
/>
</BaseModal.Content>
<BaseModal.Footer>
<Button
disabled={nameLists.includes(name) && name !== currentFlow!.name}
onClick={handleClick}
type="submit"
data-testid="save-flow-settings-button"
>
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

@ -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";
@ -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,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

@ -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) {
@ -276,6 +279,10 @@ export default function NodeToolbarComponent({
event.preventDefault();
handleSelectChange("update");
}
if (selected && event.key.toUpperCase() === "ESCAPE") {
event.preventDefault();
handleSelectChange("unselect");
}
if (
selected &&
isGroup &&
@ -380,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">
@ -433,7 +440,7 @@ export default function NodeToolbarComponent({
<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();
@ -454,7 +461,7 @@ 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>
@ -491,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

@ -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

@ -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;

View file

@ -0,0 +1,48 @@
import { useContext } from "react";
import {
API_ERROR_ALERT,
API_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants";
import { AuthContext } from "../../../../contexts/authContext";
import { addApiKeyStore } from "../../../../controllers/API";
const useSaveKey = (
setSuccessData,
setErrorData,
setHasApiKey,
setValidApiKey,
setLoadingApiKey,
) => {
const { storeApiKey } = useContext(AuthContext);
const handleSaveKey = (apikey, handleInput) => {
if (apikey) {
setLoadingApiKey(true);
addApiKeyStore(apikey).then(
() => {
setSuccessData({ title: API_SUCCESS_ALERT });
storeApiKey(apikey);
setHasApiKey(true);
setValidApiKey(true);
setLoadingApiKey(false);
handleInput({ target: { name: "apikey", value: "" } });
},
(error) => {
setErrorData({
title: API_ERROR_ALERT,
list: [error.response.data.detail],
});
setHasApiKey(false);
setValidApiKey(false);
setLoadingApiKey(false);
},
);
}
};
return {
handleSaveKey,
};
};
export default useSaveKey;

View file

@ -0,0 +1,17 @@
import { useEffect } from "react";
const useScrollToElement = (scrollId, setCurrentFlowId) => {
useEffect(() => {
const element = document.getElementById(scrollId ?? "null");
if (element) {
// Scroll smoothly to the top of the next section
element.scrollIntoView({ behavior: "smooth" });
}
}, [scrollId]);
useEffect(() => {
setCurrentFlowId("");
}, [setCurrentFlowId]);
};
export default useScrollToElement;

View file

@ -0,0 +1,49 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
import { cn } from "../../../../../../utils/utils";
type HeaderMessagesComponentProps = {
selectedRows: number[];
handleRemoveMessages: () => void;
};
const HeaderMessagesComponent = ({
selectedRows,
handleRemoveMessages,
}: HeaderMessagesComponentProps) => {
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">
Messages
<ForwardedIconComponent
name="MessagesSquare"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Inspect, edit and remove messages to explore and refine model
behaviors.
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleRemoveMessages}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
</div>
</div>
</>
);
};
export default HeaderMessagesComponent;

View file

@ -0,0 +1,24 @@
import { useEffect } from "react";
import { getMessagesTable } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore";
const useMessagesTable = (setColumns) => {
const setMessages = useMessagesStore((state) => state.setMessages);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getMessagesTable("union", undefined, ["index"]);
const { columns, rows } = data;
setColumns(columns);
setMessages(rows);
} catch (error) {
console.error("Error fetching messages:", error);
}
};
fetchData();
}, []);
return null;
};
export default useMessagesTable;

View file

@ -0,0 +1,30 @@
import { deleteMessagesFn } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore";
const useRemoveMessages = (
setSelectedRows,
setSuccessData,
setErrorData,
selectedRows,
) => {
const deleteMessages = useMessagesStore((state) => state.removeMessages);
const handleRemoveMessages = async () => {
try {
await deleteMessagesFn(selectedRows);
deleteMessages(selectedRows);
setSelectedRows([]);
setSuccessData({
title: "Messages deleted successfully.",
});
} catch (error) {
setErrorData({
title: "Error deleting messages.",
});
}
};
return { handleRemoveMessages };
};
export default useRemoveMessages;

View file

@ -0,0 +1,29 @@
import { useMessagesStore } from "../../../../../stores/messagesStore";
import { Message } from "../../../../../types/messages";
import { updateMessageApi } from "../../../../../controllers/API";
const useUpdateMessage = (setSuccessData, setErrorData) => {
const updateMessage = useMessagesStore((state) => state.updateMessage);
const handleUpdate = async (data: Message) => {
try {
await updateMessageApi(data);
updateMessage(data);
// Set success message
setSuccessData({
title: "Messages updated successfully.",
});
} catch (error) {
// Set error message
setErrorData({
title: "Error updating messages.",
});
}
};
return { handleUpdate };
};
export default useUpdateMessage;

View file

@ -0,0 +1,81 @@
import {
CellEditRequestEvent,
ColDef,
ColGroupDef,
SelectionChangedEvent,
} from "ag-grid-community";
import { useState } from "react";
import TableComponent from "../../../../components/tableComponent";
import { Card, CardContent } from "../../../../components/ui/card";
import useAlertStore from "../../../../stores/alertStore";
import { useMessagesStore } from "../../../../stores/messagesStore";
import HeaderMessagesComponent from "./components/headerMessages";
import useMessagesTable from "./hooks/use-messages-table";
import useRemoveMessages from "./hooks/use-remove-messages";
import useUpdateMessage from "./hooks/use-updateMessage";
export default function MessagesPage() {
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const messages = useMessagesStore((state) => state.messages);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const { handleRemoveMessages } = useRemoveMessages(
setSelectedRows,
setSuccessData,
setErrorData,
selectedRows,
);
const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
useMessagesTable(setColumns);
function handleUpdateMessage(event: CellEditRequestEvent<any, string>) {
const newValue = event.newValue;
const field = event.column.getColId();
const row = event.data;
const data = {
...row,
[field]: newValue,
};
handleUpdate(data);
}
return (
<div className="flex h-full w-full flex-col justify-between gap-6">
<HeaderMessagesComponent
selectedRows={selectedRows}
handleRemoveMessages={handleRemoveMessages}
/>
<div className="flex h-full w-full flex-col justify-between pb-8">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
<CardContent className="h-full">
<TableComponent
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);
}}
editable={["Sender Name", "Message"]}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(
event.api.getSelectedRows().map((row) => row.index),
);
}}
rowSelection="multiple"
suppressRowClickSelection={true}
pagination={true}
columnDefs={columns}
rowData={messages}
/>
</CardContent>
</Card>
</div>
</div>
);
}

View file

@ -29,7 +29,6 @@ import {
import { STORE_DESC, STORE_TITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/storeApiKeyModal";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
@ -180,24 +179,21 @@ export default function StorePage(): JSX.Element {
title={STORE_TITLE}
description={STORE_DESC}
button={
<>
{StoreApiKeyModal && (
<StoreApiKeyModal disabled={loading}>
<Button
data-testid="api-key-button-store"
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : "",
)}
variant="primary"
>
<IconComponent name="Key" className="mr-2 w-4" />
API Key
</Button>
</StoreApiKeyModal>
<Button
data-testid="api-key-button-store"
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : "",
)}
</>
variant="primary"
onClick={() => {
navigate("/settings/general/api");
}}
>
<IconComponent name="Key" className="mr-2 w-4" />
API Key
</Button>
}
>
<div className="flex h-full w-full flex-col justify-between">

View file

@ -1,87 +1,107 @@
import { Suspense, lazy } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { ProtectedAdminRoute } from "./components/authAdminGuard";
import { ProtectedRoute } from "./components/authGuard";
import { ProtectedLoginRoute } from "./components/authLoginGuard";
import { CatchAllRoute } from "./components/catchAllRoutes";
import LoadingComponent from "./components/loadingComponent";
import { StoreGuard } from "./components/storeGuard";
import AdminPage from "./pages/AdminPage";
import LoginAdminPage from "./pages/AdminPage/LoginPage";
import ApiKeysPage from "./pages/ApiKeysPage";
import DeleteAccountPage from "./pages/DeleteAccountPage";
import FlowPage from "./pages/FlowPage";
import LoginPage from "./pages/LoginPage";
import MyCollectionComponent from "./pages/MainPage/components/myCollectionComponent";
import HomePage from "./pages/MainPage/pages/mainPage";
import PlaygroundPage from "./pages/Playground";
import SettingsPage from "./pages/SettingsPage";
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";
import GlobalVariablesPage from "./pages/SettingsPage/pages/GlobalVariablesPage";
import ShortcutsPage from "./pages/SettingsPage/pages/ShortcutsPage";
import SignUp from "./pages/SignUpPage";
import StorePage from "./pages/StorePage";
import ViewPage from "./pages/ViewPage";
import MessagesPage from "./pages/SettingsPage/pages/messagesPage";
const AdminPage = lazy(() => import("./pages/AdminPage"));
const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage"));
const ApiKeysPage = lazy(() => import("./pages/ApiKeysPage"));
const DeleteAccountPage = lazy(() => import("./pages/DeleteAccountPage"));
const FlowPage = lazy(() => import("./pages/FlowPage"));
const LoginPage = lazy(() => import("./pages/LoginPage"));
const MyCollectionComponent = lazy(
() => import("./pages/MainPage/components/myCollectionComponent"),
);
const HomePage = lazy(() => import("./pages/MainPage/pages/mainPage"));
const PlaygroundPage = lazy(() => import("./pages/Playground"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
const GeneralPage = lazy(
() => import("./pages/SettingsPage/pages/GeneralPage"),
);
const GlobalVariablesPage = lazy(
() => import("./pages/SettingsPage/pages/GlobalVariablesPage"),
);
const ShortcutsPage = lazy(
() => import("./pages/SettingsPage/pages/ShortcutsPage"),
);
const SignUp = lazy(() => import("./pages/SignUpPage"));
const StorePage = lazy(() => import("./pages/StorePage"));
const ViewPage = lazy(() => import("./pages/ViewPage"));
const Router = () => {
return (
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"all"} />} />
<Suspense
fallback={
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
}
>
<Routes>
<Route
path="flows/*"
element={<MyCollectionComponent key="flows" type="flow" />}
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"all"} />} />
<Route
path="flows/*"
element={<MyCollectionComponent key="flows" type="flow" />}
/>
<Route
path="components/*"
element={
<MyCollectionComponent key="components" type="component" />
}
/>
<Route
path="all/*"
element={<MyCollectionComponent key="all" type="all" />}
/>
</Route>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="general/:scrollId?" element={<GeneralPage />} />
<Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} />
</Route>
<Route
path="/store"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route
path="components/*"
element={<MyCollectionComponent key="components" type="component" />}
path="/store/:id/"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route
path="all/*"
element={<MyCollectionComponent key="all" type="all" />}
/>
</Route>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="general" element={<GeneralPage />} />
<Route path="shortcuts" element={<ShortcutsPage />} />
</Route>
<Route
path="/store"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route
path="/store/:id/"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route path="/playground/:id/">
element=
{
<Route path="/playground/:id/">
<Route
path=""
element={
@ -90,96 +110,96 @@ const Router = () => {
</ProtectedRoute>
}
/>
}
</Route>
<Route path="/flow/:id/">
</Route>
<Route path="/flow/:id/">
<Route
path="*"
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
<Route
path=""
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
<Route
path="view"
element={
<ProtectedRoute>
<ViewPage />
</ProtectedRoute>
}
/>
</Route>
<Route
path="*"
element={
<ProtectedRoute>
<FlowPage />
<CatchAllRoute />
</ProtectedRoute>
}
/>
<Route
path=""
path="/login"
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="view"
path="/signup"
element={
<ProtectedRoute>
<ViewPage />
</ProtectedRoute>
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
</Route>
<Route
path="*"
element={
<ProtectedRoute>
<CatchAllRoute />
</ProtectedRoute>
}
/>
<Route
path="/login"
element={
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/signup"
element={
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
<Route
path="/login/admin"
element={
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
/>
<Route path="/account">
<Route
path="delete"
path="/login/admin"
element={
<ProtectedRoute>
<DeleteAccountPage />
</ProtectedRoute>
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
></Route>
/>
<Route
path="api-keys"
path="/admin"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
></Route>
</Route>
</Routes>
/>
<Route path="/account">
<Route
path="delete"
element={
<ProtectedRoute>
<DeleteAccountPage />
</ProtectedRoute>
}
></Route>
<Route
path="api-keys"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
}
></Route>
</Route>
</Routes>
</Suspense>
);
};

View file

@ -79,7 +79,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
buildId?: string
buildId?: string,
) => {
let newFlowPool = cloneDeep({ ...get().flowPool });
if (!newFlowPool[nodeId]) {
@ -95,7 +95,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}
//update data results
else {
newFlowPool[nodeId][index].data.messages[0] = (data as ChatOutputType| chatInputType);
newFlowPool[nodeId][index].data.messages[0] = data as
| ChatOutputType
| chatInputType;
}
}
get().setFlowPool(newFlowPool);
@ -431,11 +433,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
stopNodeId,
input_value,
files,
silent,
}: {
startNodeId?: string;
stopNodeId?: string;
input_value?: string;
files?: string[];
silent?: boolean;
}) => {
get().setIsBuilding(true);
const currentFlow = useFlowsManagerStore.getState().currentFlow;
@ -516,7 +520,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
get().addDataToFlowPool(
{ ...vertexBuildData, run_id: runId },
vertexBuildData.id
vertexBuildData.id,
);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
@ -539,19 +543,23 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
startNodeId,
stopNodeId,
onGetOrderSuccess: () => {
setNoticeData({ title: "Running components" });
if (!silent) {
setNoticeData({ title: "Running components" });
}
},
onBuildComplete: (allNodesValid) => {
const nodeId = startNodeId || stopNodeId;
if (nodeId && allNodesValid) {
setSuccessData({
title: `${
get().nodes.find((node) => node.id === nodeId)?.data.node
?.display_name
} built successfully`,
});
} else {
setSuccessData({ title: FLOW_BUILD_SUCCESS_ALERT });
if (!silent) {
if (nodeId && allNodesValid) {
setSuccessData({
title: `${
get().nodes.find((node) => node.id === nodeId)?.data.node
?.display_name
} built successfully`,
});
} else {
setSuccessData({ title: FLOW_BUILD_SUCCESS_ALERT });
}
}
get().setIsBuilding(false);
},

View file

@ -1,4 +1,5 @@
import { cloneDeep, debounce } from "lodash";
import { cloneDeep } from "lodash";
import * as debounce from "debounce-promise";
import { Edge, Node, Viewport, XYPosition } from "reactflow";
import { create } from "zustand";
import { SAVE_DEBOUNCE_TIME } from "../constants/constants";
@ -86,12 +87,12 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (dbData) {
const { data, flows } = processFlows(dbData, false);
const examples = flows.filter(
(flow) => flow.folder_id === starterFolderId
(flow) => flow.folder_id === starterFolderId,
);
get().setExamples(examples);
const flowsWithoutStarterFolder = flows.filter(
(flow) => flow.folder_id !== starterFolderId
(flow) => flow.folder_id !== starterFolderId,
);
get().setFlows(flowsWithoutStarterFolder);
@ -119,7 +120,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (get().currentFlow) {
get().saveFlow(
{ ...get().currentFlow!, data: { nodes, edges, viewport } },
true
true,
);
}
},
@ -145,7 +146,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return updatedFlow;
}
return flow;
})
}),
);
//update tabs state
@ -194,7 +195,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
flow?: FlowType,
override?: boolean,
position?: XYPosition,
fromDragAndDrop?: boolean
fromDragAndDrop?: boolean,
): Promise<string | undefined> => {
if (newProject) {
let flowData = flow
@ -210,7 +211,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
const newFlow = createNewFlow(
flowData!,
flow!,
folder_id || my_collection_id!
folder_id || my_collection_id!,
);
const { id } = await saveFlowToDatabase(newFlow);
newFlow.id = id;
@ -233,7 +234,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
const newFlow = createNewFlow(
flowData!,
flow!,
folder_id || my_collection_id!
folder_id || my_collection_id!,
);
const newName = addVersionToDuplicates(newFlow, get().flows);
@ -269,7 +270,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
.getState()
.paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
position ?? { x: 10, y: 10 }
position ?? { x: 10, y: 10 },
);
}
},
@ -279,7 +280,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
multipleDeleteFlowsComponents(id)
.then(() => {
const { data, flows } = processFlows(
get().flows.filter((flow) => !id.includes(flow.id))
get().flows.filter((flow) => !id.includes(flow.id)),
);
get().setFlows(flows);
set({ isLoading: false });
@ -299,7 +300,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
deleteFlowFromDatabase(id)
.then(() => {
const { data, flows } = processFlows(
get().flows.filter((flow) => flow.id !== id)
get().flows.filter((flow) => flow.id !== id),
);
get().setFlows(flows);
set({ isLoading: false });
@ -321,7 +322,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return new Promise<void>((resolve) => {
let componentFlow = get().flows.find(
(componentFlow) =>
componentFlow.is_component && componentFlow.name === key
componentFlow.is_component && componentFlow.name === key,
);
if (componentFlow) {
@ -369,7 +370,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
fileData,
undefined,
position,
true
true,
);
resolve(id);
}
@ -410,7 +411,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return get().addFlow(
true,
createFlowComponent(component, useDarkStore.getState().version),
override
override,
);
},
takeSnapshot: () => {
@ -431,7 +432,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (pastLength > 0) {
past[currentFlowId] = past[currentFlowId].slice(
pastLength - defaultOptions.maxHistorySize + 1,
pastLength
pastLength,
);
past[currentFlowId].push(newState);

View file

@ -0,0 +1,43 @@
import { create } from "zustand";
import { MessagesStoreType } from "../types/zustand/messages";
export const useMessagesStore = create<MessagesStoreType>((set, get) => ({
messages: [],
setMessages: (messages) => {
set(() => ({ messages: messages }));
},
addMessage: (message) => {
set(() => ({ messages: [...get().messages, message] }));
},
removeMessage: (message) => {
set(() => ({
messages: get().messages.filter((msg) => msg.id !== message.id),
}));
},
updateMessage: (message) => {
set(() => ({
messages: get().messages.map((msg) =>
msg.index === message.index ? message : msg,
),
}));
},
clearMessages: () => {
set(() => ({ messages: [] }));
},
removeMessages: (ids) => {
return new Promise((resolve, reject) => {
try {
set((state) => {
const updatedMessages = state.messages.filter(
(msg) => !ids.includes(msg.index),
);
get().setMessages(updatedMessages);
resolve(updatedMessages);
return { messages: updatedMessages };
});
} catch (error) {
reject(error);
}
});
},
}));

View file

@ -15,6 +15,10 @@ pre {
font-family: inherit;
}
.ag-paging-page-size {
display: none;
}
.react-flow__pane {
cursor: default;
}

View file

@ -239,6 +239,7 @@ export type AccordionComponentType = {
children?: ReactElement;
open?: string[];
trigger?: string | ReactElement;
disabled?: boolean;
keyValue?: string;
openDisc?: boolean;
sideBar?: boolean;
@ -396,6 +397,7 @@ export type patchUserInputStateType = {
password: string;
cnfPassword: string;
gradient: string;
apikey: string;
};
export type UserInputType = {
@ -543,7 +545,7 @@ export type nodeToolbarPropsType = {
updateNodeCode?: (
newNodeClass: APIClassType,
code: string,
name: string
name: string,
) => void;
setShowState: (show: boolean | SetStateAction<boolean>) => void;
isOutdated?: boolean;
@ -593,7 +595,7 @@ export type chatMessagePropsType = {
updateChat: (
chat: ChatMessageType,
message: string,
stream_url?: string
stream_url?: string,
) => void;
};
@ -685,12 +687,12 @@ export type codeTabsPropsType = {
value: string,
node: NodeType,
template: TemplateVariableType,
tweak: tweakType
tweak: tweakType,
) => string;
buildTweakObject?: (
tw: string,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType
template: TemplateVariableType,
) => Promise<string | void>;
};
activeTweaks?: boolean;
@ -788,3 +790,17 @@ export type toolbarSelectItemProps = {
dataTestId: string;
ping?: boolean;
};
export type clearChatPropsType = {
lockChat: boolean;
setLockChat: (lock: boolean) => void;
setChatHistory: (chatHistory: ChatMessageType) => void;
method: string;
};
export type handleSelectPropsType = {
event: string;
lockChat: boolean;
setLockChat: (lock: boolean) => void;
setChatHistory: (chatHistory: ChatMessageType) => void;
};

View file

@ -0,0 +1,13 @@
type Message = {
artifacts: Record<string, any>;
flow_id: string;
index: number;
message: string;
sender: string;
sender_name: string;
session_id: string;
timestamp: string;
id: string;
};
export type { Message };

View file

@ -68,8 +68,16 @@ export type FlowStoreType = {
onFlowPage: boolean;
setOnFlowPage: (onFlowPage: boolean) => void;
flowPool: FlowPoolType;
inputs: Array<{ type: string; id: string; displayName: string }>;
outputs: Array<{ type: string; id: string; displayName: string }>;
inputs: Array<{
type: string;
id: string;
displayName: string;
}>;
outputs: Array<{
type: string;
id: string;
displayName: string;
}>;
hasIO: boolean;
setFlowPool: (flowPool: FlowPoolType) => void;
addDataToFlowPool: (data: VertexBuildTypeAPI, nodeId: string) => void;
@ -127,12 +135,13 @@ export type FlowStoreType = {
stopNodeId,
input_value,
files,
silent,
}: {
nodeId?: string;
startNodeId?: string;
stopNodeId?: string;
input_value?: string;
files?: string[];
silent?: boolean;
}) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
updateVerticesBuild: (
@ -159,7 +168,7 @@ export type FlowStoreType = {
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
buildId?: string
buildId?: string,
) => void;
getNodePosition: (nodeId: string) => { x: number; y: number };
};

View file

@ -0,0 +1,11 @@
import { Message } from "../../messages";
export type MessagesStoreType = {
messages: Message[];
setMessages: (messages: Message[]) => void;
addMessage: (message: Message) => void;
removeMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
clearMessages: () => void;
removeMessages: (ids: number[]) => void;
};

View file

@ -21,8 +21,16 @@ export default function cloneFLowWithParent(
}
export function getInputsAndOutputs(nodes: Node[]) {
let inputs: { type: string; id: string; displayName: string }[] = [];
let outputs: { type: string; id: string; displayName: string }[] = [];
let inputs: {
type: string;
id: string;
displayName: string;
}[] = [];
let outputs: {
type: string;
id: string;
displayName: string;
}[] = [];
nodes.forEach((node) => {
const nodeData: NodeDataType = node.data as NodeDataType;
if (isOutputNode(nodeData)) {

View file

@ -144,6 +144,8 @@ import {
X,
XCircle,
Zap,
RotateCcw,
Settings,
} from "lucide-react";
import { FaApple, FaDiscord, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS";
@ -528,4 +530,6 @@ export const nodeIconsLucide: iconsType = {
FolderPlusIcon,
FolderIcon,
Discord: FaDiscord,
RotateCcw,
Settings,
};

View file

@ -352,8 +352,9 @@ export function isTimeStampString(str: string): boolean {
export function extractColumnsFromRows(
rows: object[],
mode: "intersection" | "union" = "union",
excludeColumns?: Array<string>,
): (ColDef<any> | ColGroupDef<any>)[] {
const columnsKeys: { [key: string]: ColDef<any> | ColGroupDef<any> } = {};
let columnsKeys: { [key: string]: ColDef<any> | ColGroupDef<any> } = {};
if (rows.length === 0) {
return [];
}
@ -395,5 +396,11 @@ export function extractColumnsFromRows(
union();
}
if (excludeColumns) {
for (const key of excludeColumns) {
delete columnsKeys[key];
}
}
return Object.values(columnsKeys);
}

View file

@ -59,6 +59,9 @@ test("user must interact with chat with Input/Output", async ({ page }) => {
.fill(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
);
await page.getByTestId("icon-LucideSend").click();
await page.getByText("Close", { exact: true }).click();
await page
.getByTestId("popover-anchor-input-sender_name")
.nth(1)
@ -68,7 +71,7 @@ test("user must interact with chat with Input/Output", async ({ page }) => {
.nth(0)
.fill("TestSenderNameAI");
await page.getByText("Playground", { exact: true }).click();
await page.getByText("Playground", { exact: true }).last().click();
await page.getByTestId("icon-LucideSend").click();
valueUser = await page

View file

@ -11,6 +11,7 @@ test("shoud delete a flow", async ({ page }) => {
.fill(process.env.STORE_API_KEY ?? "");
await page.getByText("Save").last().click();
await page.waitForTimeout(8000);
await page.getByText("Store").nth(0).click();
await page.getByTestId("install-Website Content QA").click();
await page.waitForTimeout(5000);

View file

@ -170,7 +170,7 @@ test("dropDownComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
value = await page.getByTestId("dropdown-model_id").innerText();
if (value !== "ai21.j2-mid-v1") {

View file

@ -175,7 +175,6 @@ test("LLMChain - Filter", async ({ page }) => {
await expect(page.getByTestId("model_specsChatOllama")).not.toBeVisible();
await expect(page.getByTestId("model_specsChatOpenAI")).not.toBeVisible();
await expect(page.getByTestId("model_specsChatVertexAI")).not.toBeVisible();
await expect(page.getByTestId("model_specsCohere")).not.toBeVisible();
await page
.locator(

View file

@ -132,7 +132,7 @@ test("FloatComponent", async ({ page }) => {
await page.locator('//*[@id="showrepeat_last_n"]').isChecked(),
).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
const plusButtonLocator = page.locator('//*[@id="float-input"]');
const elementCount = await plusButtonLocator?.count();
@ -148,7 +148,7 @@ test("FloatComponent", async ({ page }) => {
await page.locator('//*[@id="showtemperature"]').isChecked(),
).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("3");

View file

@ -44,7 +44,9 @@ test("flowSettings", async ({ page }) => {
"Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test Flow Name Test",
);
await page.getByText("Save").last().click();
await page.getByTestId("save-flow-settings").click();
await page.getByText("Close").last().click();
await page.getByText("Close").last().click();

View file

@ -31,7 +31,7 @@ test("CRUD folders", async ({ page }) => {
await page.getByText("All").first().isVisible();
await page.getByText("Select All").isVisible();
await page.getByText("New Folder", { exact: true }).first().click();
await page.getByTestId("add-folder-button").click();
await page.getByText("New Folder").last().isVisible();
await page.waitForTimeout(1000);
await page.getByText("New Folder").last().dblclick();
@ -114,7 +114,7 @@ test("change flow folder", async ({ page }) => {
await page.getByText("All").first().isVisible();
await page.getByText("Select All").isVisible();
await page.getByText("New Folder", { exact: true }).first().click();
await page.getByTestId("add-folder-button").click();
await page.getByText("New Folder").last().isVisible();
await page.waitForTimeout(1000);
await page.getByText("New Folder").last().dblclick();

View file

@ -138,7 +138,7 @@ test("InputComponent", async ({ page }) => {
.getByTestId("popover-anchor-input-collection_name-edit")
.fill("NEW_collection_name_test_123123123!@#$&*(&%$@ÇÇÇÀõe");
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
const plusButtonLocator = page.getByTestId("input-collection_name");
const elementCount = await plusButtonLocator?.count();
@ -155,7 +155,7 @@ test("InputComponent", async ({ page }) => {
await page.locator('//*[@id="showcollection_name"]').isChecked(),
).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
let value = await page
.getByTestId("popover-anchor-input-collection_name")

View file

@ -68,7 +68,7 @@ test("InputListComponent", async ({ page }) => {
.getByTestId("input-list-input-edit_metadata_indexing_include-1")
.fill("test1 test1 test1 test1");
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
await page
.getByTestId("input-list-input_metadata_indexing_include-0")

View file

@ -160,7 +160,7 @@ test("IntComponent", async ({ page }) => {
await page.locator('//*[@id="showtemperature"]').isChecked(),
).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
const plusButtonLocator = page.getByTestId("int-input-max_tokens");
const elementCount = await plusButtonLocator?.count();
@ -183,7 +183,7 @@ test("IntComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByTestId("int-input-max_tokens").click();
await page.getByTestId("int-input-max_tokens").fill("3");

View file

@ -83,7 +83,7 @@ test("KeypairListComponent", async ({ page }) => {
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked(),
).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator?.count();
@ -108,7 +108,7 @@ test("KeypairListComponent", async ({ page }) => {
const elementKeyCount = await keyPairVerification?.count();
if (elementKeyCount === 1) {
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByTestId("div-generic-node").click();

View file

@ -47,11 +47,11 @@ test("LangflowShortcuts", async ({ page }) => {
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTestId("title-Ollama").click();
await page.getByTestId("generic-node-title-arrangement").click();
await page.keyboard.press(`${control}+Shift+A`);
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
await page.getByTestId("title-Ollama").click();
await page.getByTestId("generic-node-title-arrangement").click();
await page.keyboard.press(`${control}+d`);
let numberOfNodes = await page.getByTestId("title-Ollama")?.count();
@ -71,7 +71,7 @@ test("LangflowShortcuts", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.getByTestId("title-Ollama").click();
await page.getByTestId("generic-node-title-arrangement").click();
await page.keyboard.press(`${control}+c`);
await page.getByTestId("title-Ollama").click();

View file

@ -30,6 +30,8 @@ test("should able to see and interact with logs", async ({ page }) => {
await page.getByText("No Data Available", { exact: true }).isVisible();
await page.keyboard.press("Escape");
await page.getByText("Close").last().click();
await page
.getByTestId("popover-anchor-input-openai_api_key")
.fill(process.env.OPENAI_API_KEY ?? "");

View file

@ -189,5 +189,5 @@ test("NestedComponent", async ({ page }) => {
await page.locator('//*[@id="showtext_key"]').isChecked(),
).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.getByText("Save Changes", { exact: true }).click();
});

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