feat: adds new Edit Details popover, removes flow menu, fixes nav alignment, adds new Flow Status overlay (#8087)

* Updated flow settings component size

* Added FlowSettingsComponent to contain modal content

* Removed unused imports

* Changed Flow Settings Modal to use new component

* Changed Flow Menu styling, removing Saved and context menu, and adding a direct click to edit flow info

* Removed unused styling

* Updated nav position and truncation

* updated alert styling

* Added z index to header

* Added flow settings coming from the bottom

* Changed flow settings to not crash when there is no flow

* Removed unused imports

* Implemented flow details using popover

* Removed onClick

* Changed canvas controls position and color

* Changed panel tooltip side and classes

* Added log canvas component

* Added children to flow logs modal

* Added log canvas component into page

* Changed position and shadow of canvas controls

* removed endpoint name from edit flow settings

* added endpoint name change into tweaks modal

* Added endpoint editing to tweaks

* Implemented storing the error in the flowBuildStatus

* Updated type

* Added Flow Building Component

* Added Flow Building Component implementation

* Added red color

* Added past build flow params

* Implemented design of flowBuildingComponent

* Implemented build error storing on flowStore

* Implemented build error on flow store

* Changed notifications test

* Set build error as null when building

* Reset build error when exiting flow

* Changed from error to buildError

* Changed flowStore to have buildInfo instead of buildError

* Changed flowBuildingComponent to have buildInfo and display successful builds

* Added handleDismissed instead of setting dismissed as true

* Updated tests to current Update implementation

* Updated tests to remove click on built successfully

* Updated tests and data-testid to match new Flow Name editing behavior

* fixed auto login test

* Fixed edit-flow-name test and save changes on node

* fixed tests

* Changed Share to Publish and added test ids

* added Rename Flow util for tests

* Changed tests to use new RenameFlow

* Fixed auto save off

* Added data test id to flow building component

* Removed pulsing from Name Invalid

* Made name editable but not saveable when invalid

* Added character name reached on description

* Added transition on pencil

* Modularized alert store to separate notification history and notifications

* Added errors to notification history

* Fixed flow building component position and update all components

* Fixed animations

* Fixed animation

* Added same animation to Update All Components

* Updated animations to make update only appear when flow building is not appearing

* fix flow settings test

* Fixed build status not being redefined

*  (UpdateAllComponents/index.tsx): Refactor containerVariants to CONTAINER_VARIANTS for consistency and readability
📝 (visual-variants.ts): Add visual variants for buttons and time in flowBuildingComponent
♻️ (flowBuildingComponent/index.tsx): Import visual variants from separate file for better organization and maintainability

* Fixed offset width of time

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2025-05-22 11:44:25 -03:00 committed by GitHub
commit 984b172d5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1067 additions and 1155 deletions

View file

@ -1,55 +1,37 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useAddFlow from "@/hooks/flows/use-add-flow";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { useHotkeys } from "react-hotkeys-hook";
import { memo, useMemo, useRef, useState } from "react";
import IconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import FlowSettingsComponent from "@/components/core/flowSettingsComponent";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { UPLOAD_ERROR_ALERT } from "@/constants/alerts_constants";
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { SAVED_HOVER } from "@/constants/constants";
import { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows/use-get-refresh-flows-query";
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { useUnsavedChanges } from "@/hooks/use-unsaved-changes";
import ExportModal from "@/modals/exportModal";
import FlowLogsModal from "@/modals/flowLogsModal";
import FlowSettingsModal from "@/modals/flowSettingsModal";
import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { useShortcutsStore } from "@/stores/shortcuts";
import { swatchColors } from "@/utils/styleUtils";
import { cn, getNumberFromString } from "@/utils/utils";
import { useQueryClient } from "@tanstack/react-query";
import { useHotkeys } from "react-hotkeys-hook";
import { useShallow } from "zustand/react/shallow";
export const MenuBar = memo((): JSX.Element => {
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const addFlow = useAddFlow();
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const undo = useFlowsManagerStore((state) => state.undo);
const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const [openLogs, setOpenLogs] = useState(false);
const uploadFlow = useUploadFlow();
const navigate = useCustomNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
const saveFlow = useSaveFlow();
const queryClient = useQueryClient();
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const {
currentFlowName,
@ -72,19 +54,10 @@ export const MenuBar = memo((): JSX.Element => {
})),
);
const onFlowPage = useFlowStore((state) => state.onFlowPage);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const stopBuilding = useFlowStore((state) => state.stopBuilding);
const [editingName, setEditingName] = useState(false);
const [flowName, setFlowName] = useState(currentFlowName ?? "");
const [isInvalidName, setIsInvalidName] = useState(false);
const nameInputRef = useRef<HTMLInputElement>(null);
const [inputWidth, setInputWidth] = useState<number>(0);
const measureRef = useRef<HTMLSpanElement>(null);
const changesNotSaved = useUnsavedChanges();
const [flowNames, setFlowNames] = useState<string[]>([]);
const { data: folders, isFetched: isFoldersFetched } = useGetFoldersQuery();
const flows = useFlowsManagerStore((state) => state.flows);
useGetRefreshFlowsQuery(
{
@ -99,41 +72,6 @@ export const MenuBar = memo((): JSX.Element => {
[folders, currentFlowFolderId],
);
function handleAddFlow() {
try {
addFlow().then((id) => {
setCurrentFlow(undefined); // Reset current flow for useEffect of flowPage to update the current flow
navigate("/flow/" + id);
});
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
}
function handleReloadComponents() {
queryClient.prefetchQuery({ queryKey: ["useGetTypes"] }).then(() => {
setSuccessData({ title: "Components reloaded successfully" });
});
}
function printByBuildStatus() {
if (isBuilding) {
return <div className="truncate">Building...</div>;
} else if (saveLoading) {
return <div className="truncate">Saving...</div>;
}
// return savedText;
return (
<div
data-testid="menu_status_saved_flow_button"
id="menu_status_saved_flow_button"
className="shrink-0 text-sm font-medium text-accent-emerald-foreground"
>
Saved
</div>
);
}
const handleSave = () => {
saveFlow().then(() => {
setSuccessData({ title: "Saved successfully" });
@ -143,90 +81,6 @@ export const MenuBar = memo((): JSX.Element => {
const changes = useShortcutsStore((state) => state.changesSave);
useHotkeys(changes, handleSave, { preventDefault: true });
const handleEditName = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
const invalid = flowNames.includes(value);
setIsInvalidName(invalid);
setFlowName(value);
},
[flowNames],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
setEditingName(false);
setFlowName(currentFlowName ?? "");
setIsInvalidName(false);
}
if (e.key === "Enter") {
nameInputRef.current?.blur();
}
},
[currentFlowName],
);
const handleNameSubmit = useCallback(async () => {
if (
flowName.trim() !== "" &&
flowName !== currentFlowName &&
!isInvalidName
) {
const currentFlowSnapshot = useFlowStore.getState().currentFlow;
const newFlow = {
...currentFlowSnapshot!,
name: flowName,
id: currentFlowId!,
};
saveFlow(newFlow)
.then(() => {
setCurrentFlow(newFlow);
setSuccessData({ title: "Flow name updated successfully" });
})
.catch((error) => {
setErrorData({
title: "Error updating flow name",
list: [(error as Error).message],
});
setFlowName(currentFlowName ?? "");
});
} else if (isInvalidName) {
setErrorData({
title: "Invalid flow name",
list: ["Name already exists"],
});
setFlowName(currentFlowName ?? "");
} else {
setFlowName(currentFlowName ?? "");
}
setEditingName(false);
setIsInvalidName(false);
}, [
flowName,
currentFlowName,
currentFlowId,
setCurrentFlow,
saveFlow,
setSuccessData,
setErrorData,
isInvalidName,
]);
useEffect(() => {
if (!editingName) {
setFlowName(currentFlowName ?? "Untitled Flow");
}
}, [currentFlowName, editingName]);
useEffect(() => {
if (measureRef.current) {
setInputWidth(measureRef.current.offsetWidth + 10);
}
}, [flowName, onFlowPage]);
const swatchIndex =
(currentFlowGradient && !isNaN(parseInt(currentFlowGradient))
? parseInt(currentFlowGradient)
@ -234,331 +88,118 @@ export const MenuBar = memo((): JSX.Element => {
swatchColors.length;
return onFlowPage ? (
<div
className="flex w-full items-center justify-center gap-2"
data-testid="menu_bar_wrapper"
>
<div
className="header-menu-bar hidden w-20 max-w-fit grow justify-end truncate md:flex"
data-testid="menu_flow_bar"
id="menu_flow_bar_navigation"
>
{currentFolder?.name && (
<div className="hidden truncate md:flex">
<div
className="cursor-pointer truncate pr-1 text-sm text-muted-foreground hover:text-primary"
onClick={() => {
navigate(
currentFolder?.id
? "/all/folder/" + currentFolder.id
: "/all",
);
}}
>
{currentFolder?.name}
</div>
</div>
)}
</div>
<div
className="hidden w-fit shrink-0 select-none font-normal text-muted-foreground md:flex"
data-testid="menu_bar_separator"
>
/
</div>
<div className={cn(`flex rounded p-1`, swatchColors[swatchIndex])}>
<IconComponent
name={currentFlowIcon ?? "Workflow"}
className="h-3.5 w-3.5"
/>
</div>
<div
className="shrink-0 overflow-hidden text-sm sm:whitespace-normal"
data-testid="menu_bar_display"
>
<Popover open={openSettings} onOpenChange={setOpenSettings}>
<PopoverAnchor>
<div
className="header-menu-bar-display-2 shrink-0"
data-testid="menu_bar_display_wrapper"
className="relative flex w-full items-center justify-center gap-2"
data-testid="menu_bar_wrapper"
>
<div
className="header-menu-flow-name-2 shrink-0"
data-testid="flow-configuration-button"
className="header-menu-bar hidden max-w-40 justify-end truncate md:flex xl:max-w-full"
data-testid="menu_flow_bar"
id="menu_flow_bar_navigation"
>
{currentFolder?.name && (
<div className="hidden truncate md:flex">
<div
className="cursor-pointer truncate text-sm text-muted-foreground hover:text-primary"
onClick={() => {
navigate(
currentFolder?.id
? "/all/folder/" + currentFolder.id
: "/all",
);
}}
>
{currentFolder?.name}
</div>
</div>
)}
</div>
<div
className="hidden w-fit shrink-0 select-none font-normal text-muted-foreground md:flex"
data-testid="menu_bar_separator"
>
/
</div>
<div className={cn(`flex rounded p-1`, swatchColors[swatchIndex])}>
<IconComponent
name={currentFlowIcon ?? "Workflow"}
className="h-3.5 w-3.5"
/>
</div>
<PopoverTrigger asChild>
<div
className="relative inline-flex"
style={{ width: Math.max(10, inputWidth) }}
className="group relative -mr-5 flex shrink-0 cursor-pointer items-center gap-2 text-sm sm:whitespace-normal"
data-testid="menu_bar_display"
>
<Input
className={cn(
"text- h-6 w-full shrink-0 cursor-text font-semibold",
"bg-transparent pl-1 pr-0 transition-colors duration-200",
"border-0 outline-none focus:border-0 focus:outline-none focus:ring-0 focus:ring-offset-0",
!editingName && "text-primary hover:opacity-80",
isInvalidName && "text-status-red",
)}
onChange={handleEditName}
maxLength={38}
ref={nameInputRef}
onKeyDown={handleKeyDown}
onFocus={() => {
setEditingName(true);
setFlowName(currentFlowName ?? "Untitled Flow");
const flows = useFlowsManagerStore.getState().flows;
setFlowNames(
flows
?.map((flow) => flow.name)
.filter((name) => name !== currentFlowName) ?? [],
);
}}
onBlur={handleNameSubmit}
value={flowName}
id="input-flow-name"
data-testid="input-flow-name"
placeholder="Untitled Flow"
/>
<span
ref={measureRef}
className="invisible absolute left-0 top-0 -z-10 w-fit whitespace-pre text-sm font-semibold"
className="w-fit max-w-[35vw] truncate whitespace-pre text-mmd font-semibold sm:max-w-full sm:text-sm"
aria-hidden="true"
data-testid="flow_name"
>
{flowName || "Untitled Flow"}
{currentFlowName || "Untitled Flow"}
</span>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger
className="group"
data-testid="flow_menu_trigger"
>
<IconComponent
name="ChevronDown"
className="flex h-5 w-5 text-muted-foreground hover:text-primary"
name="pencil"
className={cn(
"h-5 w-3.5 -translate-x-2 opacity-0 transition-all",
!openSettings &&
"sm:group-hover:translate-x-0 sm:group-hover:opacity-100",
)}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-44 bg-white dark:bg-background">
<DropdownMenuLabel>Options</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
handleAddFlow();
}}
className="cursor-pointer"
data-testid="menu_new_flow_button"
id="menu_new_flow_button"
>
<IconComponent name="Plus" className="header-menu-options" />
New
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOpenSettings(true);
}}
className="cursor-pointer"
data-testid="menu_edit_flow_button"
id="menu_edit_flow_button"
>
<IconComponent
name="SquarePen"
className="header-menu-options"
/>
Edit Details
</DropdownMenuItem>
{!autoSaving && (
<DropdownMenuItem
onClick={handleSave}
className="cursor-pointer"
data-testid="menu_save_flow_button"
id="menu_save_flow_button"
>
<ToolbarSelectItem
value="Save"
icon="Save"
dataTestId=""
shortcut={
shortcuts.find(
(s) => s.name.toLowerCase() === "changes save",
)?.shortcut!
}
/>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setOpenLogs(true);
}}
className="cursor-pointer"
data-testid="menu_logs_flow_button"
id="menu_logs_flow_button"
>
<IconComponent
name="ScrollText"
className="header-menu-options"
/>
Logs
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
uploadFlow({ position: { x: 300, y: 100 } })
.then(() => {
setSuccessData({
title: "Uploaded successfully",
});
})
.catch((error) => {
setErrorData({
title: UPLOAD_ERROR_ALERT,
list: [(error as Error).message],
});
});
}}
data-testid="menu_import_flow_button"
id="menu_import_flow_button"
>
<IconComponent name="FileUp" className="header-menu-options" />
Import
</DropdownMenuItem>
<ExportModal>
<div className="header-menubar-item">
<IconComponent
name="FileDown"
className="header-menu-options"
/>
Export
</div>
</ExportModal>
<DropdownMenuItem
onClick={() => {
undo();
}}
className="cursor-pointer"
data-testid="menu_undo_flow_button"
id="menu_undo_flow_button"
>
<ToolbarSelectItem
value="Undo"
icon="Undo"
dataTestId=""
shortcut={
shortcuts.find((s) => s.name.toLowerCase() === "undo")
?.shortcut!
}
/>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
redo();
}}
className="cursor-pointer"
data-testid="menu_redo_flow_button"
id="menu_redo_flow_button"
>
<ToolbarSelectItem
value="Redo"
icon="Redo"
dataTestId=""
shortcut={
shortcuts.find((s) => s.name.toLowerCase() === "redo")
?.shortcut!
}
/>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleReloadComponents();
}}
className="cursor-pointer"
data-testid="menu_refresh_flow_button"
id="menu_refresh_flow_button"
>
<IconComponent
name="RefreshCcw"
className="header-menu-options"
/>
Refresh All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<FlowSettingsModal
open={openSettings}
setOpen={setOpenSettings}
></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div>
<div className={"hidden w-28 shrink-0 items-center sm:flex"}>
{!autoSaving && (
<Button
variant="primary"
size="icon"
disabled={autoSaving || !changesNotSaved || isBuilding}
className={cn("mr-1 h-9 px-2")}
onClick={handleSave}
data-testid="save-flow-button"
>
<IconComponent name={"Save"} className={cn("h-5 w-5")} />
</Button>
)}
<ShadTooltip
content={
autoSaving ? (
SAVED_HOVER +
(updatedAt
? new Date(updatedAt).toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
})
: "Never")
) : (
<div className="flex w-48 flex-col gap-1 py-1">
<h2 className="text-base font-semibold">
Auto-saving is disabled
</h2>
<p className="text-muted-foreground">
<a
href="https://docs.langflow.org/configuration-auto-save"
className="text-secondary underline"
>
Enable auto-saving
</a>{" "}
to avoid losing progress.
</p>
</div>
)
}
side="bottom"
styleClasses="cursor-default z-10"
>
<div className="flex cursor-default items-center gap-2 truncate text-sm text-muted-foreground">
<div className="flex cursor-default items-center gap-2 truncate text-sm">
<div className="w-full truncate text-sm">
{printByBuildStatus()}
</div>
</div>
<button
data-testid="stop_building_button"
disabled={!isBuilding}
onClick={(_) => {
if (isBuilding) {
stopBuilding();
</PopoverTrigger>
<div className={"ml-5 hidden shrink-0 items-center sm:flex"}>
{!autoSaving && (
<ShadTooltip
content={
changesNotSaved
? saveLoading
? "Saving..."
: "Save Changes"
: SAVED_HOVER +
(updatedAt
? new Date(updatedAt).toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
})
: "Never")
}
}}
className={
isBuilding
? "hidden items-center gap-1.5 text-sm text-status-red sm:flex"
: "hidden"
}
>
<IconComponent name="Square" className="h-4 w-4" />
<span>Stop</span>
</button>
side="bottom"
styleClasses="cursor-default z-10"
>
<div>
<Button
variant="primary"
size="iconMd"
disabled={!changesNotSaved || isBuilding || saveLoading}
className={cn("h-7 w-7 border-border")}
onClick={handleSave}
data-testid="save-flow-button"
>
<IconComponent
name={saveLoading ? "Loader2" : "Save"}
className={cn("h-5 w-5", saveLoading && "animate-spin")}
/>
</Button>
</div>
</ShadTooltip>
)}
</div>
</ShadTooltip>
</div>
</div>
</div>
</PopoverAnchor>
<PopoverContent
className="flex w-96 flex-col gap-4 p-4"
align="center"
sideOffset={15}
>
<span className="text-sm font-semibold">Flow Details</span>
<FlowSettingsComponent close={() => setOpenSettings(false)} />
</PopoverContent>
</Popover>
) : (
<></>
);

View file

@ -16,7 +16,7 @@ export const HeaderMenu = ({ children }) => (
export const HeaderMenuToggle = ({ children }) => (
<DropdownMenuTrigger
className="inline-flex w-full items-center justify-center rounded-md pl-4 pr-1"
className="inline-flex w-full items-center justify-center rounded-md pl-1 pr-1"
data-testid="user_menu_button"
id="user_menu_button"
>

View file

@ -14,9 +14,7 @@ import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useTheme from "@/customization/hooks/use-custom-theme";
import useAlertStore from "@/stores/alertStore";
import { useEffect, useRef, useState } from "react";
import { AccountMenu } from "./components/AccountMenu";
import FlowMenu from "./components/FlowMenu";
import LangflowCounts from "./components/langflow-counts";
export default function AppHeader(): JSX.Element {
const notificationCenter = useAlertStore((state) => state.notificationCenter);
@ -47,13 +45,13 @@ export default function AppHeader(): JSX.Element {
const getNotificationBadge = () => {
const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive";
return notificationCenter
? `${baseClasses} right-[5.1rem] top-[5px]`
? `${baseClasses} right-[0.3rem] top-[5px]`
: "hidden";
};
return (
<div
className={`flex h-[48px] w-full items-center justify-between border-b px-6 dark:bg-background`}
className={`z-10 flex h-[48px] w-full items-center justify-between border-b px-6 dark:bg-background`}
data-testid="app-header"
>
{/* Left Section */}
@ -82,13 +80,13 @@ export default function AppHeader(): JSX.Element {
</div>
{/* Middle Section */}
<div className="w-full flex-1 truncate lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<div className="absolute left-1/2 w-full flex-1 -translate-x-1/2">
<FlowMenu />
</div>
{/* Right Section */}
<div
className={`relative left-3 z-30 flex items-center gap-1`}
className={`relative left-3 z-30 flex items-center gap-3`}
data-testid="header_right_section_wrapper"
>
<>
@ -119,7 +117,7 @@ export default function AppHeader(): JSX.Element {
}
data-testid="notification_button"
>
<div className="hit-area-hover group items-center rounded-md px-2 py-1 text-muted-foreground">
<div className="hit-area-hover group relative items-center rounded-md px-2 py-2 text-muted-foreground">
<span className={getNotificationBadge()} />
<ForwardedIconComponent
name="Bell"
@ -140,7 +138,7 @@ export default function AppHeader(): JSX.Element {
</AlertDropdown>
<Separator
orientation="vertical"
className="my-auto ml-3 h-7 dark:border-zinc-700"
className="my-auto h-7 dark:border-zinc-700"
/>
<div className="flex">

View file

@ -39,17 +39,20 @@ export const CustomControlButton = ({
return (
<ControlButton
data-testid={testId}
className="!h-8 !w-8 rounded !p-0"
className="group !h-8 !w-8 rounded !p-0"
onClick={onClick}
disabled={disabled}
title={testId?.replace(/_/g, " ")}
>
<ShadTooltip content={tooltipText}>
<ShadTooltip content={tooltipText} side="left">
<div className={cn("rounded p-2.5", backgroundClasses)}>
<IconComponent
name={iconName}
aria-hidden="true"
className={cn("scale-150 text-muted-foreground", iconClasses)}
className={cn(
"scale-150 text-muted-foreground group-hover:text-primary",
iconClasses,
)}
/>
</div>
</ShadTooltip>
@ -109,7 +112,7 @@ const CanvasControls = ({ children }) => {
return (
<Panel
data-testid="canvas_controls"
className="react-flow__controls !left-auto !m-2 flex !flex-row gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent"
className="react-flow__controls !left-auto !m-2 flex !flex-col gap-1.5 rounded-md border border-border bg-background fill-foreground stroke-foreground p-0.5 text-primary [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent"
position="bottom-left"
>
{/* Zoom In */}
@ -135,6 +138,7 @@ const CanvasControls = ({ children }) => {
onClick={fitView}
testId="fit_view"
/>
{children}
{/* Lock/Unlock */}
<CustomControlButton
iconName={isInteractive ? "LockOpen" : "Lock"}
@ -146,7 +150,6 @@ const CanvasControls = ({ children }) => {
}
testId="lock_unlock"
/>
{children}
</Panel>
);
};

View file

@ -1,6 +1,6 @@
import React, { ChangeEvent, useState } from "react";
import { InputProps } from "../../../types/components";
import { cn, isEndpointNameValid } from "../../../utils/utils";
import { cn } from "../../../utils/utils";
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { Textarea } from "../../ui/textarea";
@ -9,16 +9,15 @@ export const EditFlowSettings: React.FC<InputProps> = ({
name,
invalidNameList = [],
description,
endpointName,
maxLength = 50,
descriptionMaxLength = 250,
minLength = 1,
setName,
setDescription,
setEndpointName,
}: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false);
const [isMinLength, setIsMinLength] = useState(false);
const [validEndpointName, setValidEndpointName] = useState(true);
const [isInvalidName, setIsInvalidName] = useState(false);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -43,34 +42,22 @@ export const EditFlowSettings: React.FC<InputProps> = ({
}
setIsInvalidName(invalid);
// Only update the name if it's valid (not empty and not invalid)
if (value.length >= minLength && !invalid) {
setName!(value);
} else if (value.length === 0) {
setName!(value);
if (value.length === 0) {
// For empty string, update state but keep isMinLength true
setName!("");
setIsMinLength(true);
}
};
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription!(event.target.value);
};
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
// Validate the endpoint name
// use this regex r'^[a-zA-Z0-9_-]+$'
const isValid = isEndpointNameValid(event.target.value, maxLength);
setValidEndpointName(isValid);
// Only update if valid and meets minimum length (if set)
if (isValid && value.length >= minLength) {
setEndpointName!(value);
} else if (value.length === 0) {
// Always allow empty endpoint name (it's optional)
setEndpointName!("");
if (value.length >= descriptionMaxLength) {
setIsMaxDescriptionLength(true);
} else {
setIsMaxDescriptionLength(false);
}
setDescription!(value);
};
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
@ -80,7 +67,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
<>
<Label>
<div className="edit-flow-arrangement">
<span className="font-medium">Name{setName ? "" : ":"}</span>{" "}
<span className="text-mmd font-medium">Name{setName ? "" : ":"}</span>{" "}
{isMaxLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
@ -90,9 +77,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
</span>
)}
{isInvalidName && (
<span className="edit-flow-span">
Name invalid or already exists
</span>
<span className="edit-flow-span">Flow name already exists</span>
)}
</div>
{setName ? (
@ -120,9 +105,12 @@ export const EditFlowSettings: React.FC<InputProps> = ({
</Label>
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium">
Description{setDescription ? " (optional)" : ":"}
<span className="text-mmd font-medium">
Description{setDescription ? "" : ":"}
</span>
{isMaxDescriptionLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
</div>
{setDescription ? (
<Textarea
@ -131,8 +119,10 @@ export const EditFlowSettings: React.FC<InputProps> = ({
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
data-testid="input-flow-description"
className="mt-2 max-h-[250px] resize-none font-normal"
rows={5}
maxLength={descriptionMaxLength}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
@ -148,33 +138,6 @@ export const EditFlowSettings: React.FC<InputProps> = ({
</div>
)}
</Label>
{setEndpointName && (
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium">Endpoint Name</span>
{!validEndpointName && (
<span className="edit-flow-span">
Invalid endpoint name. Use only letters, numbers, hyphens, and
underscores ({maxLength} characters max).
</span>
)}
</div>
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleEndpointNameChange}
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name to run the endpoint"
maxLength={maxLength}
minLength={minLength}
id="endpoint_name"
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/>
</Label>
)}
</>
);
};

View file

@ -0,0 +1,116 @@
import { Button } from "@/components/ui/button";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { FlowType } from "@/types/flow";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import EditFlowSettings from "../editFlowSettingsComponent";
export default function FlowSettingsComponent({
flowData,
close,
}: {
flowData?: FlowType;
close: () => void;
}): JSX.Element {
const saveFlow = useSaveFlow();
const currentFlow = useFlowStore((state) =>
flowData ? undefined : state.currentFlow,
);
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const flows = useFlowsManagerStore((state) => state.flows);
const flow = flowData ?? currentFlow;
const [name, setName] = useState(flow?.name ?? "");
const [description, setDescription] = useState(flow?.description ?? "");
const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
useEffect(() => {
setName(flow?.name ?? "");
setDescription(flow?.description ?? "");
}, [flow?.name, flow?.description, flow?.endpoint_name, open]);
function handleClick(): void {
setIsSaving(true);
if (!flow) return;
const newFlow = cloneDeep(flow);
newFlow.name = name;
newFlow.description = description;
if (autoSaving) {
saveFlow(newFlow)
?.then(() => {
setIsSaving(false);
setSuccessData({ title: "Changes saved successfully" });
close();
})
.catch(() => {
setIsSaving(false);
});
} else {
setCurrentFlow(newFlow);
setIsSaving(false);
close();
}
}
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
if (flows) {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
tempNameList.push(flow?.name ?? "");
});
setNameList(tempNameList.filter((name) => name !== (flow?.name ?? "")));
}
}, [flows]);
useEffect(() => {
if (
(!nameLists.includes(name) && flow?.name !== name) ||
flow?.description !== description
) {
setDisableSave(false);
} else {
setDisableSave(true);
}
}, [nameLists, flow, description, name]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<EditFlowSettings
invalidNameList={nameLists}
name={name}
description={description}
setName={setName}
setDescription={setDescription}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
data-testid="cancel-flow-settings"
onClick={() => close()}
>
Cancel
</Button>
<Button
variant="default"
size="sm"
data-testid="save-flow-settings"
onClick={handleClick}
loading={isSaving}
disabled={disableSave}
>
Save
</Button>
</div>
</div>
);
}

View file

@ -85,15 +85,13 @@ export default function PublishDropdown() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
className="!h-8 !w-[95px] font-medium"
variant="ghost"
size="md"
className="!px-2.5 font-medium"
data-testid="publish-button"
>
Publish
<IconComponent
name="ChevronDown"
className="icon-size font-medium"
/>
Share
<IconComponent name="ChevronDown" className="!h-5 !w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent

View file

@ -0,0 +1,27 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import FlowLogsModal from "@/modals/flowLogsModal";
import { Panel } from "@xyflow/react";
const LogCanvasControls = () => {
return (
<Panel
data-testid="canvas_controls"
className="react-flow__controls !m-2 rounded-md"
position="bottom-right"
>
<FlowLogsModal>
<Button
variant="primary"
size="sm"
className="flex items-center !gap-1.5"
>
<ForwardedIconComponent name="Terminal" className="text-primary" />
<span className="text-mmd font-normal">Logs</span>
</Button>
</FlowLogsModal>
</Panel>
);
};
export default LogCanvasControls;

View file

@ -1,19 +1,29 @@
import { TweaksComponent } from "@/components/core/codeTabsComponent/components/tweaksComponent";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { CustomAPIGenerator } from "@/customization/components/custom-api-generator";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAuthStore from "@/stores/authStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { isEndpointNameValid } from "@/utils/utils";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import { ReactNode, useEffect, useState } from "react";
import { cloneDeep } from "lodash";
import { ChangeEvent, ReactNode, useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import IconComponent from "../../components/common/genericIconComponent";
import { useTweaksStore } from "../../stores/tweaksStore";
import BaseModal from "../baseModal";
import APITabsComponent from "./codeTabs/code-tabs";
const MAX_LENGTH = 20;
const MIN_LENGTH = 1;
export default function ApiModal({
children,
open: myOpen,
@ -33,10 +43,57 @@ export default function ApiModal({
: useState(false);
const newInitialSetup = useTweaksStore((state) => state.newInitialSetup);
const flowEndpointName = useFlowStore(
useShallow((state) => state.currentFlow?.endpoint_name),
);
const [endpointName, setEndpointName] = useState(flowEndpointName ?? "");
const [validEndpointName, setValidEndpointName] = useState(true);
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
// Validate the endpoint name
// use this regex r'^[a-zA-Z0-9_-]+$'
const isValid = isEndpointNameValid(event.target.value, MAX_LENGTH);
setValidEndpointName(isValid);
// Only update if valid and meets minimum length (if set)
if (isValid && value.length >= MIN_LENGTH) {
setEndpointName!(value);
} else if (value.length === 0) {
// Always allow empty endpoint name (it's optional)
setEndpointName!("");
}
};
useEffect(() => {
if (open) newInitialSetup(nodes);
}, [open]);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const saveFlow = useSaveFlow();
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
function handleSave(): void {
const newFlow = cloneDeep(useFlowStore.getState().currentFlow);
if (!newFlow) return;
newFlow.endpoint_name =
endpointName && endpointName.length > 0 ? endpointName : null;
if (autoSaving) {
saveFlow(newFlow);
} else {
setCurrentFlow(newFlow);
}
}
useEffect(() => {
if (!openTweaks && endpointName !== flowEndpointName) handleSave();
else if (openTweaks) {
setEndpointName(flowEndpointName ?? "");
}
}, [openTweaks]);
return (
<>
<BaseModal
@ -131,7 +188,33 @@ export default function ApiModal({
/>
<span className="pl-2">Tweaks</span>
</BaseModal.Header>
<BaseModal.Content overflowHidden>
<BaseModal.Content overflowHidden className="flex flex-col gap-4">
{true && (
<Label>
<div className="edit-flow-arrangement mt-2">
<span className="shrink-0 text-mmd font-medium">
Endpoint Name
</span>
{!validEndpointName && (
<span className="edit-flow-span">
Use only letters, numbers, hyphens, and underscores (
{MAX_LENGTH} characters max).
</span>
)}
</div>
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleEndpointNameChange}
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name to run the endpoint"
maxLength={MAX_LENGTH}
minLength={MIN_LENGTH}
id="endpoint_name"
/>
</Label>
)}
<div className="h-full w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll">
<TweaksComponent open={openTweaks} />
</div>

View file

@ -3,7 +3,6 @@ import PaginatorComponent from "@/components/common/paginatorComponent";
import TableComponent from "@/components/core/parameterRenderComponent/components/tableComponent";
import { useGetTransactionsQuery } from "@/controllers/API/queries/transactions";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { FlowSettingsPropsType } from "@/types/components";
import { convertUTCToLocalTimezone } from "@/utils/utils";
import { ColDef, ColGroupDef } from "ag-grid-community";
import { useCallback, useEffect, useState } from "react";
@ -11,10 +10,12 @@ import { useSearchParams } from "react-router-dom";
import BaseModal from "../baseModal";
export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [open, setOpen] = useState(false);
const [pageIndex, setPageIndex] = useState(1);
const [pageSize, setPageSize] = useState(20);
@ -60,6 +61,7 @@ export default function FlowLogsModal({
return (
<BaseModal open={open} setOpen={setOpen} size="x-large">
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description="Inspect component executions.">
<div className="flex w-full justify-between">
<div className="flex h-fit w-32 items-center">

View file

@ -1,127 +1,29 @@
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import IconComponent from "../../components/common/genericIconComponent";
import EditFlowSettings from "../../components/core/editFlowSettingsComponent";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import FlowSettingsComponent from "@/components/core/flowSettingsComponent";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import { isEndpointNameValid } from "../../utils/utils";
import BaseModal from "../baseModal";
export default function FlowSettingsModal({
open,
setOpen,
flowData,
details,
}: FlowSettingsPropsType): JSX.Element {
if (!open) return <></>;
const saveFlow = useSaveFlow();
const currentFlow = useFlowStore((state) =>
flowData ? undefined : state.currentFlow,
);
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const flows = useFlowsManagerStore((state) => state.flows);
const flow = flowData ?? currentFlow;
const [name, setName] = useState(flow?.name ?? "");
const [description, setDescription] = useState(flow?.description ?? "");
const [endpoint_name, setEndpointName] = useState(flow?.endpoint_name ?? "");
const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
useEffect(() => {
setName(flow?.name ?? "");
setDescription(flow?.description ?? "");
setEndpointName(flow?.endpoint_name ?? "");
}, [flow?.name, flow?.description, flow?.endpoint_name, open]);
function handleClick(): void {
setIsSaving(true);
if (!flow) return;
const newFlow = cloneDeep(flow);
newFlow.name = name;
newFlow.description = description;
newFlow.endpoint_name =
endpoint_name && endpoint_name.length > 0 ? endpoint_name : null;
if (autoSaving) {
saveFlow(newFlow)
?.then(() => {
setOpen(false);
setIsSaving(false);
setSuccessData({ title: "Changes saved successfully" });
})
.catch(() => {
setIsSaving(false);
});
} else {
setCurrentFlow(newFlow);
setOpen(false);
setIsSaving(false);
}
}
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
if (flows) {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== flow!.name));
}
}, [flows]);
useEffect(() => {
if (
(!nameLists.includes(name) && flow?.name !== name) ||
flow?.description !== description ||
((flow?.endpoint_name ?? "") !== endpoint_name &&
isEndpointNameValid(endpoint_name ?? "", 50))
) {
setDisableSave(false);
} else {
setDisableSave(true);
}
}, [nameLists, flow, description, endpoint_name, name]);
return (
<BaseModal
open={open}
setOpen={setOpen}
size="smaller-h-full"
onSubmit={handleClick}
size="small-update"
className="p-4"
>
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
<span className="pr-2">Details</span>
<IconComponent name="SquarePen" className="mr-2 h-4 w-4" />
<BaseModal.Header>
<span className="text-base font-semibold">Flow Details</span>
</BaseModal.Header>
<BaseModal.Content>
<EditFlowSettings
invalidNameList={nameLists}
name={name}
description={description}
endpointName={endpoint_name}
setName={setName}
setDescription={setDescription}
setEndpointName={details ? undefined : setEndpointName}
<FlowSettingsComponent
flowData={flowData}
close={() => setOpen(false)}
/>
</BaseModal.Content>
<BaseModal.Footer
submit={{
label: "Save",
dataTestId: "save-flow-settings",
disabled: disableSave,
loading: isSaving,
}}
/>
</BaseModal>
);
}

View file

@ -2,6 +2,7 @@ import ForwardedIconComponent from "@/components/common/genericIconComponent";
import CanvasControls, {
CustomControlButton,
} from "@/components/core/canvasControlsComponent";
import LogCanvasControls from "@/components/core/logCanvasControlsComponent";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { cn } from "@/utils/utils";
import { Background, Panel } from "@xyflow/react";
@ -18,6 +19,8 @@ interface MemoizedCanvasControlsProps {
shadowBoxHeight: number;
}
export const MemoizedLogCanvasControls = memo(() => <LogCanvasControls />);
export const MemoizedCanvasControls = memo(
({
setIsAddingNote,
@ -38,7 +41,6 @@ export const MemoizedCanvasControls = memo(
shadowBox.style.top = `${position.y - shadowBoxHeight / 2}px`;
}
}}
iconClasses="text-primary"
testId="add_note"
/>
</CanvasControls>

View file

@ -23,6 +23,7 @@ import {
reconnectEdge,
SelectionDragHandler,
} from "@xyflow/react";
import { AnimatePresence } from "framer-motion";
import _, { cloneDeep } from "lodash";
import {
KeyboardEvent,
@ -59,9 +60,11 @@ import {
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import UpdateAllComponents from "../UpdateAllComponents";
import FlowBuildingComponent from "../flowBuildingComponent";
import {
MemoizedBackground,
MemoizedCanvasControls,
MemoizedLogCanvasControls,
MemoizedSidebarTrigger,
} from "./MemoizedComponents";
import getRandomName from "./utils/get-random-name";
@ -559,6 +562,7 @@ export default function Page({
<div id="react-flow-id" className="h-full w-full bg-canvas">
{!view && (
<>
<MemoizedLogCanvasControls />
<MemoizedCanvasControls
setIsAddingNote={setIsAddingNote}
position={position.current}
@ -569,9 +573,6 @@ export default function Page({
</>
)}
<MemoizedSidebarTrigger />
<div className={cn(componentsToUpdate.length === 0 && "hidden")}>
<UpdateAllComponents />
</div>
<SelectionMenu
lastSelection={lastSelection}
isVisible={selectionMenuVisible}
@ -616,6 +617,8 @@ export default function Page({
onPaneClick={onPaneClick}
onEdgeClick={handleEdgeClick}
>
<FlowBuildingComponent />
<UpdateAllComponents />
<MemoizedBackground />
</ReactFlow>
<div

View file

@ -11,6 +11,7 @@ import useFlowStore from "@/stores/flowStore";
import { useTypesStore } from "@/stores/typesStore";
import { cn } from "@/utils/utils";
import { useUpdateNodeInternals } from "@xyflow/react";
import { AnimatePresence, motion } from "framer-motion";
import { useMemo, useRef, useState } from "react";
const ERROR_MESSAGE_UPDATING_COMPONENTS = "Error updating components";
@ -21,6 +22,12 @@ const ERROR_MESSAGE_UPDATING_COMPONENTS_LIST = [
const ERROR_MESSAGE_EDGES_LOST =
"Some edges were lost after updating the components. Please review the flow and reconnect them.";
const CONTAINER_VARIANTS = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
};
export default function UpdateAllComponents({}: {}) {
const { componentsToUpdate, nodes, edges, setNodes } = useFlowStore();
const templates = useTypesStore((state) => state.templates);
@ -28,6 +35,9 @@ export default function UpdateAllComponents({}: {}) {
const { mutateAsync: validateComponentCode } = usePostValidateComponentCode();
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const isBuilding = useFlowStore((state) => state.isBuilding);
const buildInfo = useFlowStore((state) => state.buildInfo);
const updateNodeInternals = useUpdateNodeInternals();
const updateAllNodes = useUpdateAllNodes(setNodes, updateNodeInternals);
@ -176,59 +186,76 @@ export default function UpdateAllComponents({}: {}) {
};
};
const handleDismissAllComponents = (
e: React.MouseEvent<HTMLButtonElement>,
) => {
addDismissedNodes(
componentsToUpdateFiltered.map((component) => component.id),
);
e.stopPropagation();
};
if (componentsToUpdateFiltered.length === 0) return null;
return (
<div
className={cn(
"absolute bottom-2 left-1/2 z-50 flex w-[530px] -translate-x-1/2 items-center justify-between gap-8 rounded-lg border bg-background px-4 py-2 text-sm font-medium shadow-md transition-all ease-in",
dismissed && "translate-y-[120%]",
componentsToUpdateFiltered.some(
(component) => component.breakingChange,
) && "border-accent-amber-foreground",
)}
>
<div className="flex items-center gap-3">
<span>
Update
{componentsToUpdateFiltered.length > 1 ? "s are" : " is"} available
for{" "}
{componentsToUpdateFiltered.length +
" component" +
(componentsToUpdateFiltered.length > 1 ? "s" : "")}
</span>
</div>
<div className="flex items-center gap-4">
<Button
variant="link"
size="icon"
className="shrink-0 text-sm"
onClick={(e) => {
addDismissedNodes(
componentsToUpdateFiltered.map((component) => component.id),
);
e.stopPropagation();
}}
>
Dismiss {componentsToUpdateFiltered.length > 1 ? "All" : ""}
</Button>
<Button
size="sm"
className="shrink-0"
onClick={() => handleUpdateAllComponents()}
loading={loadingUpdate}
data-testid="update-all-button"
>
{breakingChanges.length > 0 ? "Review All" : "Update All"}
</Button>
</div>
<UpdateComponentModal
isMultiple={true}
open={isOpen}
setOpen={setIsOpen}
onUpdateNode={(ids) => handleUpdateAllComponents(true, ids)}
components={componentsToUpdateFiltered}
/>
</div>
<AnimatePresence mode="wait">
{!dismissed &&
!isBuilding &&
!buildInfo?.error &&
!buildInfo?.success && (
<div className="absolute bottom-2 left-1/2 z-50 w-[530px] -translate-x-1/2">
<motion.div
initial="hidden"
animate="visible"
exit="exit"
variants={CONTAINER_VARIANTS}
transition={{ duration: 0.2, ease: "easeOut" }}
className={cn(
"flex items-center justify-between gap-8 rounded-lg border bg-background px-4 py-2 text-sm font-medium shadow-md",
componentsToUpdateFiltered.some(
(component) => component.breakingChange,
) && "border-accent-amber-foreground",
)}
>
<div className="flex items-center gap-3">
<span>
Update
{componentsToUpdateFiltered.length > 1 ? "s are" : " is"}{" "}
available for{" "}
{componentsToUpdateFiltered.length +
" component" +
(componentsToUpdateFiltered.length > 1 ? "s" : "")}
</span>
</div>
<div className="flex items-center gap-4">
<Button
variant="link"
size="icon"
className="shrink-0 text-sm"
onClick={handleDismissAllComponents}
>
Dismiss {componentsToUpdateFiltered.length > 1 ? "All" : ""}
</Button>
<Button
size="sm"
className="shrink-0"
onClick={() => handleUpdateAllComponents()}
loading={loadingUpdate}
data-testid="update-all-button"
>
{breakingChanges.length > 0 ? "Review All" : "Update All"}
</Button>
</div>
<UpdateComponentModal
isMultiple={true}
open={isOpen}
setOpen={setIsOpen}
onUpdateNode={(ids) => handleUpdateAllComponents(true, ids)}
components={componentsToUpdateFiltered}
/>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,31 @@
export const CONTAINER_VARIANTS = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
};
export const STOP_BUTTON_VARIANTS = {
hidden: { opacity: 0, x: 0 },
visible: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 0 },
};
export const RETRY_BUTTON_VARIANTS = {
hidden: { opacity: 0, x: 10 },
visible: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 10 },
};
export const DISMISS_BUTTON_VARIANTS = {
hidden: { opacity: 0, x: 10 },
visible: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 10 },
};
export const getTimeVariants = (buttonRef: React.RefObject<HTMLDivElement>) => {
const errorButtonsWidth = buttonRef.current?.offsetWidth ?? 0;
return {
single: { x: 0, width: "auto" },
double: { x: -errorButtonsWidth - 15, width: "auto" },
};
};

View file

@ -0,0 +1,285 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { BorderTrail } from "@/components/core/border-trail";
import { Button } from "@/components/ui/button";
import { TextShimmer } from "@/components/ui/TextShimmer";
import { BuildStatus } from "@/constants/enums";
import { normalizeTimeString } from "@/CustomNodes/GenericNode/components/NodeStatus/utils/format-run-time";
import useFlowStore from "@/stores/flowStore";
import { cn } from "@/utils/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useMemo, useRef, useState } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
CONTAINER_VARIANTS,
DISMISS_BUTTON_VARIANTS,
RETRY_BUTTON_VARIANTS,
STOP_BUTTON_VARIANTS,
getTimeVariants,
} from "./helpers/visual-variants";
export default function FlowBuildingComponent() {
const isBuilding = useFlowStore((state) => state.isBuilding);
const flowBuildStatus = useFlowStore((state) => state.flowBuildStatus);
const buildInfo = useFlowStore((state) => state.buildInfo);
const errorButtonsRef = useRef<HTMLDivElement>(null);
const stopButtonRef = useRef<HTMLDivElement>(null);
const setBuildInfo = useFlowStore((state) => state.setBuildInfo);
const [duration, setDuration] = useState(0);
const [dismissed, setDismissed] = useState(false);
const stopBuilding = useFlowStore((state) => state.stopBuilding);
const prevIsBuilding = useRef(isBuilding);
const pastBuildFlowParams = useFlowStore(
(state) => state.pastBuildFlowParams,
);
const buildFlow = useFlowStore((state) => state.buildFlow);
const statusBuilding = useMemo(
() =>
Object.entries(flowBuildStatus)
.filter(([_, s]) => s.status === BuildStatus.BUILDING)
.map(([id, s]) => ({
id,
...s,
})),
[flowBuildStatus],
);
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isBuilding && !prevIsBuilding.current) {
setDismissed(false);
setDuration(0);
}
if (isBuilding) {
intervalId = setInterval(() => {
setDuration((prev) => prev + 10);
}, 10);
}
prevIsBuilding.current = isBuilding;
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [isBuilding]);
const displayTime = duration ?? 0;
const secondsValue = displayTime / 1000;
const humanizedTime =
normalizeTimeString(`${secondsValue.toFixed(1)}seconds`) ??
`${secondsValue.toFixed(1)}s`;
const buildingContent = useMemo(() => {
if (!isBuilding) return null;
return (
<TextShimmer duration={1}>
{statusBuilding.length > 0
? `Running ${statusBuilding[0]?.id}`
: "Running flow"}
</TextShimmer>
);
}, [isBuilding, statusBuilding]);
useEffect(() => {
if (buildInfo?.success) {
setTimeout(() => {
handleDismiss();
}, 2000);
}
}, [buildInfo?.success]);
const handleDismiss = () => {
setDismissed(true);
setTimeout(() => {
setBuildInfo(null);
setDismissed(false);
}, 500);
};
const handleStop = () => {
stopBuilding();
};
const handleRetry = () => {
if (pastBuildFlowParams) {
buildFlow(pastBuildFlowParams);
}
};
return (
<AnimatePresence mode="wait">
{(isBuilding || buildInfo?.error || buildInfo?.success) && !dismissed && (
<div className="absolute bottom-2 left-1/2 z-50 w-[530px] -translate-x-1/2">
<motion.div
initial="hidden"
animate="visible"
exit="exit"
variants={CONTAINER_VARIANTS}
transition={{ duration: 0.2, delay: 0.2, ease: "easeOut" }}
className={cn(
"flex flex-col justify-center overflow-hidden rounded-lg border bg-background px-4 py-2 text-sm shadow-md transition-colors duration-200",
!isBuilding &&
buildInfo?.error &&
"border-accent-red-foreground text-accent-red-foreground",
!isBuilding &&
buildInfo?.success &&
"border-accent-emerald-foreground text-accent-emerald-foreground",
)}
>
<AnimatePresence mode="wait">
{(isBuilding || buildInfo?.error || buildInfo?.success) && (
<>
{isBuilding && (
<BorderTrail
size={100}
transition={{
repeat: Infinity,
duration: 10,
ease: "linear",
}}
/>
)}
<div className="flex min-h-10 w-full items-center justify-between gap-2">
<AnimatePresence mode="wait">
<div>
{buildingContent ? (
buildingContent
) : buildInfo?.success ? (
"Flow built successfully"
) : (
<div className="flex items-center gap-2">
<ForwardedIconComponent
name="CircleAlert"
className="h-5 w-5"
/>
Flow build failed
</div>
)}
</div>
</AnimatePresence>
<div className="relative flex items-center gap-4">
<motion.div
variants={getTimeVariants(
buildInfo?.error ? errorButtonsRef : stopButtonRef,
)}
animate={!buildInfo?.success ? "double" : "single"}
transition={{
duration: 0.2,
ease: "easeOut",
}}
className="absolute right-0 font-mono text-xs"
>
{humanizedTime}
</motion.div>
<AnimatePresence mode="sync">
{!buildInfo?.success && (
<div className="absolute right-0">
{buildInfo?.error ? (
<motion.div
key="error-buttons"
ref={errorButtonsRef}
className="flex items-center gap-2"
>
<motion.div
variants={RETRY_BUTTON_VARIANTS}
initial="hidden"
animate="visible"
exit="exit"
transition={{ duration: 0.2 }}
>
<Button size="sm" onClick={handleRetry}>
Retry
</Button>
</motion.div>
<motion.div
variants={DISMISS_BUTTON_VARIANTS}
initial="hidden"
animate="visible"
exit="exit"
transition={{ duration: 0.2 }}
>
<Button
size="sm"
variant="outline"
className="text-primary"
onClick={handleDismiss}
>
Dismiss
</Button>
</motion.div>
</motion.div>
) : (
<motion.div
key="stop-button"
variants={STOP_BUTTON_VARIANTS}
ref={stopButtonRef}
initial="hidden"
animate="visible"
className=""
exit="exit"
transition={{ duration: 0.2 }}
>
<Button
data-testid="stop_building_button"
size="sm"
onClick={handleStop}
>
Stop
</Button>
</motion.div>
)}
</div>
)}
</AnimatePresence>
</div>
</div>
<AnimatePresence>
{buildInfo?.error && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<Markdown
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
className="my-1.5 align-text-top truncate-doubleline"
components={{
a: ({ node, ...props }) => (
<a
href={props.href}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
{props.children}
</a>
),
p({ node, ...props }) {
return (
<span className="inline-block w-fit max-w-full align-text-top">
{props.children}
</span>
);
},
}}
>
{buildInfo?.error?.join("\n")}
</Markdown>
</motion.div>
)}
</AnimatePresence>
</>
)}
</AnimatePresence>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View file

@ -231,7 +231,6 @@ const ListComponent = ({
open={openSettings}
setOpen={setOpenSettings}
flowData={flowData}
details
/>
</>
);

View file

@ -4,14 +4,6 @@ import { AlertItemType } from "../types/alerts";
import { AlertStoreType } from "../types/zustand/alert";
import { customStringify } from "../utils/reactflowUtils";
const pushNotificationList = (
list: AlertItemType[],
notification: AlertItemType,
) => {
list.unshift(notification);
return list;
};
const useAlertStore = create<AlertStoreType>((set, get) => ({
errorData: { title: "", list: [] },
noticeData: { title: "", link: "" },
@ -19,122 +11,72 @@ const useAlertStore = create<AlertStoreType>((set, get) => ({
notificationCenter: false,
notificationList: [],
tempNotificationList: [],
addNotificationToHistory: (notification: Omit<AlertItemType, "id">) => {
const newNotification = { ...notification, id: uniqueId() };
set({
notificationCenter: true,
notificationList: [newNotification, ...get().notificationList],
});
},
addNotificationToTempList: (notification: Omit<AlertItemType, "id">) => {
const newNotification = { ...notification, id: uniqueId() };
const tempList = get().tempNotificationList;
if (
!tempList.some((item) => {
return (
customStringify({
title: item.title,
type: item.type,
list: item.list,
link: item.link,
}) ===
customStringify({
title: newNotification.title,
type: newNotification.type,
list: newNotification.list,
link: newNotification.link,
})
);
})
) {
set({
tempNotificationList: [newNotification, ...get().tempNotificationList],
});
}
},
setErrorData: (newState: { title: string; list?: Array<string> }) => {
if (newState.title && newState.title !== "") {
set({
errorData: newState,
notificationCenter: true,
notificationList: [
{
type: "error",
title: newState.title,
list: newState.list,
id: uniqueId(),
},
...get().notificationList,
],
});
const tempList = get().tempNotificationList;
if (
!tempList.some((item) => {
return (
customStringify({
title: item.title,
type: item.type,
list: item.list,
}) === customStringify({ ...newState, type: "error" })
);
})
) {
set({
tempNotificationList: [
{
type: "error",
title: newState.title,
list: newState.list,
id: uniqueId(),
},
...get().tempNotificationList,
],
});
}
set({ errorData: newState });
const notification: Omit<AlertItemType, "id"> = {
type: "error",
title: newState.title,
list: newState.list,
};
get().addNotificationToHistory(notification);
get().addNotificationToTempList(notification);
}
},
setNoticeData: (newState: { title: string; link?: string }) => {
if (newState.title && newState.title !== "") {
set({
noticeData: newState,
notificationCenter: true,
notificationList: [
{
type: "notice",
title: newState.title,
link: newState.link,
id: uniqueId(),
},
...get().notificationList,
],
});
const tempList = get().tempNotificationList;
if (
!tempList.some((item) => {
return (
customStringify({
title: item.title,
type: item.type,
link: item.link,
}) === customStringify({ ...newState, type: "notice" })
);
})
) {
set({
tempNotificationList: [
{
type: "notice",
title: newState.title,
link: newState.link,
id: uniqueId(),
},
...get().tempNotificationList,
],
});
}
set({ noticeData: newState });
const notification: Omit<AlertItemType, "id"> = {
type: "notice",
title: newState.title,
link: newState.link,
};
get().addNotificationToHistory(notification);
get().addNotificationToTempList(notification);
}
},
setSuccessData: (newState: { title: string }) => {
if (newState.title && newState.title !== "") {
set({
successData: newState,
notificationCenter: true,
notificationList: [
{
type: "success",
title: newState.title,
id: uniqueId(),
},
...get().notificationList,
],
});
const tempList = get().tempNotificationList;
if (
!tempList.some((item) => {
return (
customStringify({ title: item.title, type: item.type }) ===
customStringify({ ...newState, type: "success" })
);
})
) {
set({
tempNotificationList: [
{
type: "success",
title: newState.title,
id: uniqueId(),
},
...get().tempNotificationList,
],
});
}
set({ successData: newState });
const notification: Omit<AlertItemType, "id"> = {
type: "success",
title: newState.title,
};
get().addNotificationToHistory(notification);
get().addNotificationToTempList(notification);
}
},
setNotificationCenter: (newState: boolean) => {

View file

@ -1,3 +1,4 @@
import { MISSED_ERROR_ALERT } from "@/constants/alerts_constants";
import { BROKEN_EDGES_WARNING } from "@/constants/constants";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import {
@ -17,10 +18,6 @@ import {
} from "@xyflow/react";
import { cloneDeep, zip } from "lodash";
import { create } from "zustand";
import {
FLOW_BUILD_SUCCESS_ALERT,
MISSED_ERROR_ALERT,
} from "../constants/alerts_constants";
import { BuildStatus, EventDeliveryType } from "../constants/enums";
import { LogsLogType, VertexBuildTypeAPI } from "../types/api";
import { ChatInputType, ChatOutputType } from "../types/chat";
@ -233,6 +230,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
nodes,
edges: newEdges,
flowState: undefined,
buildInfo: null,
inputs,
outputs,
hasIO: inputs.length > 0 || outputs.length > 0,
@ -596,6 +594,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
});
},
pastBuildFlowParams: null,
buildInfo: null,
setBuildInfo: (buildInfo: { error?: string[]; success?: boolean } | null) => {
set({ buildInfo });
},
buildFlow: async ({
startNodeId,
stopNodeId,
@ -615,27 +618,44 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
stream?: boolean;
eventDelivery?: EventDeliveryType;
}) => {
set({
pastBuildFlowParams: {
startNodeId,
stopNodeId,
input_value,
files,
silent,
session,
stream,
eventDelivery,
},
buildInfo: null,
});
const playgroundPage = get().playgroundPage;
get().setIsBuilding(true);
set({ flowBuildStatus: {} });
const currentFlow = useFlowsManagerStore.getState().currentFlow;
const setSuccessData = useAlertStore.getState().setSuccessData;
const setErrorData = useAlertStore.getState().setErrorData;
const setNoticeData = useAlertStore.getState().setNoticeData;
const edges = get().edges;
let error = false;
let errors: string[] = [];
for (const edge of edges) {
const errors = validateEdge(edge, get().nodes, edges);
if (errors.length > 0) {
const errorsEdge = validateEdge(edge, get().nodes, edges);
if (errorsEdge.length > 0) {
error = true;
setErrorData({
errors.push(errorsEdge.join("\n"));
useAlertStore.getState().addNotificationToHistory({
title: MISSED_ERROR_ALERT,
list: errors,
type: "error",
list: errorsEdge,
});
}
}
if (error) {
get().setIsBuilding(false);
get().setBuildInfo({ error: errors, success: false });
throw new Error("Invalid components");
}
@ -647,8 +667,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const errors = errorsObjs.map((obj) => obj.errors).flat();
if (errors.length > 0) {
setErrorData({
get().setBuildInfo({ error: errors, success: false });
useAlertStore.getState().addNotificationToHistory({
title: MISSED_ERROR_ALERT,
type: "error",
list: errors,
});
get().setIsBuilding(false);
@ -666,10 +688,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
) {
if (vertexBuildData && vertexBuildData.inactivated_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
get().updateBuildStatus(
vertexBuildData.inactivated_vertices,
BuildStatus.INACTIVE,
);
if (vertexBuildData.inactivated_vertices.length > 0) {
get().updateBuildStatus(
vertexBuildData.inactivated_vertices,
BuildStatus.INACTIVE,
);
}
}
if (vertexBuildData.next_vertices_ids) {
@ -754,8 +778,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
{ ...vertexBuildData, run_id: runId },
vertexBuildData.id,
);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
if (status !== BuildStatus.ERROR) {
get().updateBuildStatus([vertexBuildData.id], status);
}
}
await buildFlowVerticesWithFallback({
session,
@ -764,23 +789,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
flowId: currentFlow!.id,
startNodeId,
stopNodeId,
onGetOrderSuccess: () => {
if (!silent) {
setNoticeData({ title: "Running components" });
}
},
onGetOrderSuccess: () => {},
onBuildComplete: (allNodesValid) => {
const nodeId = startNodeId || stopNodeId;
if (!silent) {
if (allNodesValid) {
setSuccessData({
title: nodeId
? `${
get().nodes.find((node) => node.id === nodeId)?.data.node
?.display_name
} built successfully`
: FLOW_BUILD_SUCCESS_ALERT,
});
get().setBuildInfo({ success: true });
}
}
get().updateEdgesRunningByNodes(
@ -808,7 +822,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
get().nodes.map((n) => n.id),
false,
);
setErrorData({ list, title });
get().setBuildInfo({ error: list, success: false });
useAlertStore.getState().addNotificationToHistory({
title: title,
type: "error",
list: list,
});
get().setIsBuilding(false);
get().buildController.abort();
trackFlowBuild(get().currentFlow?.name ?? "Unknown", true, {
@ -991,6 +1010,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
lastCopiedSelection: null,
verticesBuild: null,
flowBuildStatus: {},
buildInfo: null,
isBuilding: false,
isPending: true,
positionDictionary: {},

View file

@ -546,7 +546,7 @@
@apply flex justify-between;
}
.edit-flow-span {
@apply ml-10 animate-pulse text-status-red;
@apply ml-8 text-mmd font-normal text-status-red;
}
.float-component-pointer {
@ -561,12 +561,6 @@
@apply flex max-w-[110px] cursor-pointer items-center gap-2 lg:max-w-[150px];
}
.header-menu-bar-display-2 {
@apply flex cursor-pointer items-center gap-2;
}
.header-menu-flow-name-2 {
@apply flex-1;
}
.header-menu-flow-name {
@apply flex-1 truncate;
}
@ -1289,7 +1283,7 @@
@apply flex h-[32px] w-[32px] items-center justify-center rounded-md bg-muted font-bold transition-all;
}
.no-focus-visible{
.no-focus-visible {
@apply focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0;
--tw-ring-offset-width: none !important;
--tw-ring-shadow: none !important;

View file

@ -41,6 +41,7 @@
--accent-emerald-hover: 152.4 76% 80.4%; /* hsl(152.4, 76%, 80.4%) */
--accent-indigo: 226 100% 94%; /* hsl(226, 100%, 94%) */
--accent-indigo-foreground: 243 75% 59%; /* hsl(243, 75%, 59%) */
--accent-red-foreground: 0 72% 51%; /* hsl(0, 72%, 51%) */
--accent-pink: 326, 78%, 95%; /* hsl(326, 78%, 95%) */
--accent-pink-foreground: 333 71% 51%; /* hsl(333, 71%, 51%) */
@ -231,6 +232,7 @@
--accent-indigo-foreground: 234 89% 74%; /* hsl(234, 89%, 74%) */
--accent-pink: 336, 69%, 30%; /* hsl(336, 69%, 30%) */
--accent-pink-foreground: 329 86% 70%; /* hsl(329, 86%, 70%) */
--accent-red-foreground: 0 91% 71%; /* hsl(0, 91%, 71%) */
--tooltip: 0 0% 100%; /* hsl(0, 0%, 100%) */
--jse-theme-color: hsl(240, 4%, 16%) !important;

View file

@ -303,6 +303,7 @@ export type InputProps = {
description: string | null;
endpointName?: string | null;
maxLength?: number;
descriptionMaxLength?: number;
minLength?: number;
setName?: (name: string) => void;
setDescription?: (description: string) => void;
@ -737,7 +738,6 @@ export type buttonBoxPropsType = {
export type FlowSettingsPropsType = {
open: boolean;
setOpen: (open: boolean) => void;
details?: boolean;
flowData?: FlowType;
};

View file

@ -15,4 +15,6 @@ export type AlertStoreType = {
removeFromTempNotificationList: (index: string) => void;
clearNotificationList: () => void;
removeFromNotificationList: (index: string) => void;
addNotificationToHistory: (notification: Omit<AlertItemType, "id">) => void;
addNotificationToTempList: (notification: Omit<AlertItemType, "id">) => void;
};

View file

@ -156,6 +156,20 @@ export type FlowStoreType = {
unselectAll: () => void;
playgroundPage: boolean;
setPlaygroundPage: (playgroundPage: boolean) => void;
buildInfo: { error?: string[]; success?: boolean } | null;
setBuildInfo: (
buildInfo: { error?: string[]; success?: boolean } | null,
) => void;
pastBuildFlowParams: {
startNodeId?: string;
stopNodeId?: string;
input_value?: string;
files?: string[];
silent?: boolean;
session?: string;
stream?: boolean;
eventDelivery?: EventDeliveryType;
} | null;
buildFlow: ({
startNodeId,
stopNodeId,
@ -163,6 +177,7 @@ export type FlowStoreType = {
files,
silent,
session,
stream,
eventDelivery,
}: {
startNodeId?: string;
@ -171,6 +186,7 @@ export type FlowStoreType = {
files?: string[];
silent?: boolean;
session?: string;
stream?: boolean;
eventDelivery?: EventDeliveryType;
}) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: EdgeType[]; viewport: Viewport };
@ -190,10 +206,13 @@ export type FlowStoreType = {
runId?: string;
verticesToRun: string[];
} | null;
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => void;
revertBuiltStatusFromBuilding: () => void;
flowBuildStatus: {
[key: string]: { status: BuildStatus; timestamp?: string };
[key: string]: {
status: BuildStatus;
timestamp?: string;
};
};
updateFlowPool: (
nodeId: string,

View file

@ -170,6 +170,7 @@ const config = {
"success-foreground": "var(--success-foreground)",
"accent-pink": "hsl(var(--accent-pink))",
"accent-pink-foreground": "hsl(var(--accent-pink-foreground))",
"accent-red-foreground": "hsl(var(--accent-red-foreground))",
filter: {
foreground: "var(--filter-foreground)",
background: "var(--filter-background)",

View file

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"when auto_login is false, admin can CRUD user's and should see just your own flows",
@ -146,12 +147,7 @@ test(
await page.getByTestId("fit_view").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details", { exact: true }).last().click();
await page.getByPlaceholder("Flow Name").fill(randomFlowName);
await page.getByText("Save", { exact: true }).click();
await renameFlow(page, { flowName: randomFlowName });
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
@ -226,12 +222,7 @@ test(
await page.getByTestId("fit_view").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details", { exact: true }).last().click();
await page.getByPlaceholder("Flow Name").fill(secondRandomFlowName);
await page.getByText("Save", { exact: true }).click();
await renameFlow(page, { flowName: secondRandomFlowName });
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,

View file

@ -45,10 +45,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page
.getByTestId("output-inspection-message-chatoutput")
.first()
@ -72,10 +68,6 @@ test(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page
.getByTestId("output-inspection-message-chatoutput")
.first()
@ -116,10 +108,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page
.getByTestId("output-inspection-message-chatoutput")
.first()

View file

@ -93,16 +93,14 @@ test(
await page.getByTestId("fit_view").click();
let outdatedComponents = await page
.getByTestId("icon-AlertTriangle")
.count();
let outdatedComponents = await page.getByTestId("update-button").count();
while (outdatedComponents > 0) {
await page.getByTestId("icon-AlertTriangle").first().click();
await page.waitForSelector('[data-testid="icon-AlertTriangle"]', {
await page.getByTestId("update-button").first().click();
await page.waitForSelector('[data-testid="update-button"]', {
timeout: 1000,
});
outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
outdatedComponents = await page.getByTestId("update-button").count();
}
let filledApiKey = await page.getByTestId("remove-icon-badge").count();
@ -157,10 +155,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.waitForSelector(
'[data-testid="output-inspection-message-chatoutput"]',
{
@ -185,10 +179,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.waitForSelector(
'[data-testid="output-inspection-message-chatoutput"]',
{
@ -242,10 +232,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.waitForSelector(
'[data-testid="output-inspection-message-chatoutput"]',
{
@ -278,10 +264,6 @@ test(
timeout: 30000 * 3,
});
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.waitForSelector(
'[data-testid="output-inspection-message-chatoutput"]',
{

View file

@ -24,13 +24,11 @@ test(
await expect(page.getByTestId(/.*rf__node.*/).first()).toBeVisible({
timeout: 3000,
});
let outdatedComponents = await page
.getByTestId("icon-AlertTriangle")
.count();
let outdatedComponents = await page.getByTestId("update-button").count();
while (outdatedComponents > 0) {
await page.getByTestId("icon-AlertTriangle").first().click();
outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
await page.getByTestId("update-button").first().click();
outdatedComponents = await page.getByTestId("update-button").count();
}
let filledApiKey = await page.getByTestId("remove-icon-badge").count();
@ -39,7 +37,6 @@ test(
filledApiKey = await page.getByTestId("remove-icon-badge").count();
}
await page.getByTestId("icon-ChevronDown").first().click();
await page.getByText("Logs").click();
await page.getByText("No Data Available", { exact: true }).isVisible();
await page.keyboard.press("Escape");
@ -61,14 +58,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page
.getByText("Chat Output built successfully", { exact: true })
.isVisible();
await page.getByTestId("icon-ChevronDown").first().click();
await page.getByText("Logs").click();
await page.getByText("timestamp").first().isVisible();

View file

@ -2,6 +2,7 @@ import { test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"should filter by tag",
@ -114,15 +115,8 @@ test("should share component with share button", async ({ page }) => {
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForTimeout(1000);
const flowName = await page.getByTestId("input-flow-name").inputValue();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").click();
const flowDescription = await page
.getByPlaceholder("Flow description")
.inputValue();
await page.getByPlaceholder("Flow name").fill(randomName);
await page.getByText("Save").last().click();
await renameFlow(page, { flowName: randomName });
await page.waitForSelector('[data-testid="shared-button-flow"]', {
timeout: 100000,
@ -148,8 +142,14 @@ test("should share component with share button", async ({ page }) => {
await page.getByText("Vector Store").first().isVisible();
await page.getByText("Prompt").last().isVisible();
await page.getByTestId("public-checkbox").isChecked();
const flowName = await page.getByTestId("input-flow-name").inputValue();
const flowDescription = await page
.getByPlaceholder("Flow description")
.inputValue();
await page.getByText(flowName).last().isVisible();
await page.getByText(flowDescription).last().isVisible();
await page.waitForTimeout(1000);
await page.getByText("Flow shared successfully").last().isVisible();
});

View file

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"flow state should be properly cleaned up between user sessions",
@ -110,13 +110,9 @@ test(
});
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="fit_view"]', { timeout: 30000 });
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details", { exact: true }).last().click();
await page.getByPlaceholder("Flow Name").fill(userAFlowName);
await page.getByText("Save", { exact: true }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 30000,
});
await renameFlow(page, { flowName: userAFlowName });
await page.getByTestId("icon-ChevronLeft").first().click();
// Verify User A can see their flow

View file

@ -28,10 +28,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -46,10 +46,6 @@ withEventDeliveryModes(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 30000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByPlaceholder(

View file

@ -34,10 +34,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -39,9 +39,7 @@ test.skip(
await page.waitForSelector("text=built successfully", {
timeout: 60000 * 3,
});
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForTimeout(1000);
expect(page.getByText("apple").last()).toBeVisible();

View file

@ -44,10 +44,6 @@ test.skip(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
expect(await page.locator(".markdown").count()).toBeGreaterThan(0);

View file

@ -46,10 +46,6 @@ test(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -39,9 +39,6 @@ test(
// Wait for the flow to build successfully
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 30000,
});
// Switch to Playground
await page.getByRole("button", { name: "Playground", exact: true }).click();

View file

@ -46,10 +46,6 @@ withEventDeliveryModes(
timeout: 60000 * 3,
});
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -27,10 +27,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page

View file

@ -35,10 +35,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -35,10 +35,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -35,10 +35,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -33,10 +33,6 @@ test.skip(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
expect(await page.locator(".markdown").count()).toBeGreaterThan(0);

View file

@ -46,10 +46,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").last().click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("Add a Chat Input component to your flow to send messages.", {

View file

@ -67,10 +67,6 @@ withEventDeliveryModes(
timeout: 60000 * 3,
});
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector("text=default session", {

View file

@ -37,10 +37,6 @@ withEventDeliveryModes(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page
.getByText("No input message provided.", { exact: true })

View file

@ -1,7 +1,6 @@
import { expect, test } from "@playwright/test";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { extractAndCleanCode } from "../../utils/extract-and-clean-code";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
@ -243,16 +242,11 @@ withEventDeliveryModes(
await page.waitForSelector("text=built successfully", {
timeout: 60000 * 2,
});
await page.getByText("built successfully").last().click({
timeout: 30000,
});
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", {
timeout: 60000 * 2,
});
await page.getByText("built successfully").last().click({
timeout: 30000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();
await page.waitForSelector('[data-testid="input-chat-playground"]', {

View file

@ -122,13 +122,11 @@ test.skip(
.getByTestId(/^rf__node-TextInput-[a-zA-Z0-9]+$/)
.getByTestId("textarea_str_input_value")
.fill("This is a test!");
let outdatedComponents = await page
.getByTestId("icon-AlertTriangle")
.count();
let outdatedComponents = await page.getByTestId("update-button").count();
while (outdatedComponents > 0) {
await page.getByTestId("icon-AlertTriangle").first().click();
await page.getByTestId("update-button").first().click();
await page.waitForTimeout(1000);
outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
outdatedComponents = await page.getByTestId("update-button").count();
}
let filledApiKey = await page.getByTestId("remove-icon-badge").count();
while (filledApiKey > 0) {

View file

@ -26,13 +26,11 @@ test(
await page.getByTestId("fit_view").click();
let outdatedComponents = await page
.getByTestId("icon-AlertTriangle")
.count();
let outdatedComponents = await page.getByTestId("update-button").count();
while (outdatedComponents > 0) {
await page.getByTestId("icon-AlertTriangle").first().click();
outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
await page.getByTestId("update-button").first().click();
outdatedComponents = await page.getByTestId("update-button").count();
}
await page.getByTestId("promptarea_prompt_template").click();

View file

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"should be able to move flow from folder, rename it and be displayed on correct folder",
@ -25,10 +26,8 @@ test(
await page.getByTestId("fit_view").click();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").click();
await page.getByPlaceholder("Flow name").fill(randomName);
await page.getByText("Save").last().click();
await renameFlow(page, { flowName: randomName });
await page.getByTestId("icon-ChevronLeft").last().click();
await page.getByTestId("add-project-button").click();
@ -72,10 +71,8 @@ test(
await page.getByTestId(`card-${randomName}`).first().click();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").click();
await page.getByPlaceholder("Flow name").fill(secondRandomName);
await page.getByText("Save").last().click();
await renameFlow(page, { flowName: secondRandomName });
await page.getByTestId("icon-ChevronLeft").last().click();
await page.waitForTimeout(3000);

View file

@ -241,10 +241,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
expect(
await page
.getByTestId("output-inspection-combined text-groupnode")

View file

@ -47,21 +47,6 @@ test(
await page.getByTestId("fit_view").click();
expect(await page.getByText("Saved").last().isVisible()).toBeTruthy();
await page
.getByText("Saved")
.first()
.hover()
.then(async () => {
await expect(
page.getByText("Auto-saving is disabled").nth(0),
).toBeVisible({ timeout: 5000 });
await expect(
page.getByText("Enable auto-saving to avoid losing progress.").nth(0),
).toBeVisible({ timeout: 3000 });
});
expect(await page.getByTestId("save-flow-button").isEnabled()).toBeTruthy();
await page.waitForSelector("text=loading", {
@ -136,7 +121,7 @@ test(
timeout: 5000,
});
await expect(page.getByTestId("title-NVIDIA")).toBeVisible({
await expect(page.getByTestId("title-NVIDIA").first()).toBeVisible({
timeout: 5000,
});

View file

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"user should be able to edit flow name by clicking on the header or on the main page",
{ tag: ["@release", "@workspace", "@components"] },
@ -13,16 +14,9 @@ test(
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.getByTestId("input-flow-name").click();
await page.waitForTimeout(1000);
await renameFlow(page, { flowName: randomName });
await page.getByTestId("input-flow-name").fill(randomName);
await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
let flowName = await page.getByTestId("input-flow-name").inputValue();
let { flowName } = await renameFlow(page);
expect(flowName).toBe(randomName);
@ -41,17 +35,11 @@ test(
await page.getByText(randomName).click();
await page.getByTestId("input-flow-name").click();
await renameFlow(page, { flowName: randomName2 });
await page.getByTestId("input-flow-name").fill(randomName2);
let { flowName: flowName2 } = await renameFlow(page);
await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
flowName = await page.getByTestId("input-flow-name").inputValue();
expect(flowName).toBe(randomName2);
expect(flowName2).toBe(randomName2);
await page.getByTestId("icon-ChevronLeft").first().click();
@ -66,13 +54,19 @@ test(
expect(await page.getByText(randomName2).count()).toBe(1);
await page.getByTestId("home-dropdown-menu").first().click();
await page.getByText(randomName2).click();
await page.getByTestId("btn-edit-flow").click();
await renameFlow(page, { flowName: randomName3 });
await page.getByTestId("input-flow-name").fill(randomName3);
let { flowName: flowName3 } = await renameFlow(page);
await page.getByTestId("save-flow-settings").click();
expect(flowName3).toBe(randomName3);
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
timeout: 5000,
});
await page.waitForSelector(`text=${randomName3}`, {
timeout: 3000,
@ -83,17 +77,11 @@ test(
await page.getByText(randomName3).click();
await page.getByTestId("input-flow-name").click();
await renameFlow(page, { flowName: randomName4 });
await page.getByTestId("input-flow-name").fill(randomName4);
let { flowName: flowName4 } = await renameFlow(page);
await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
flowName = await page.getByTestId("input-flow-name").inputValue();
expect(flowName).toBe(randomName4);
expect(flowName4).toBe(randomName4);
await page.getByTestId("icon-ChevronLeft").first().click();

View file

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test(
"flowSettings",
@ -11,53 +12,50 @@ test(
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="input-flow-name"]', {
timeout: 3000,
});
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").first().click();
await page.getByTestId("flow_name").isVisible({ timeout: 3000 });
await page.getByTestId("flow_name").click();
await page.waitForTimeout(500);
await page.getByTestId("input-flow-name").click();
await page
.getByPlaceholder("Flow name")
.getByTestId("input-flow-name")
.fill(
"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("Character limit reached").isVisible();
await page.getByPlaceholder("Flow name").click();
await page.getByTestId("input-flow-name").click();
const randomName = Math.random().toString(36).substring(2);
await page.getByPlaceholder("Flow name").fill(randomName);
await page.getByPlaceholder("Flow name").click();
await page.getByTestId("input-flow-name").fill(randomName);
await page
.getByPlaceholder("Flow description")
.getByTestId("input-flow-description")
.fill(
"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.getByTestId("save-flow-settings").isEnabled({ timeout: 3000 });
await page.getByTestId("save-flow-settings").click();
await page.getByText("Changes saved successfully").isVisible();
await page
.getByText("Changes saved successfully")
.last()
.isVisible({ timeout: 3000 });
await page.getByText("Changes saved successfully").last().click();
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").first().click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 30000,
});
const flowName = await page.getByPlaceholder("Flow name").inputValue();
const flowDescription = await page
.getByPlaceholder("Flow description")
.inputValue();
const { flowName, flowDescription } = await renameFlow(page);
if (flowName != randomName) {
expect(false).toBeTruthy();
}
expect(flowName == randomName).toBeTruthy();
if (
flowDescription !=
"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"
) {
expect(false).toBeTruthy();
}
await page.getByText("Saved").first().isVisible();
await page.getByTestId("icon-CheckCircle2").first().isVisible();
expect(
flowDescription ==
"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 ",
).toBeTruthy();
},
);

View file

@ -217,9 +217,6 @@ test(
// Build and run
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
// Verify output
await page.waitForSelector(

View file

@ -27,10 +27,6 @@ test(
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByTestId("notification_button").click();
// Add explicit waits before checking visibility
@ -39,7 +35,7 @@ test(
state: "visible",
});
await page.waitForSelector("text=Running components", {
await page.waitForSelector("text=Running", {
timeout: 30000,
state: "visible",
});
@ -52,13 +48,11 @@ test(
const trashIcon = page.getByTestId("icon-Trash2").last();
await expect(trashIcon).toBeVisible();
const runningComponentsText = page
.getByText("Running components", { exact: true })
.last();
const runningComponentsText = page.getByText("Running").last();
await expect(runningComponentsText).toBeVisible();
const builtSuccessfullyText = page
.getByText("Text Input built successfully", { exact: true })
.getByText("Flow built successfully", { exact: true })
.last();
await expect(builtSuccessfullyText).toBeVisible();
},

View file

@ -34,13 +34,6 @@ test(
timeout: 30000 * 3,
});
await page
.getByText("built successfully")
.last()
.click({
timeout: 30000 * 3,
});
await page.waitForSelector('[data-testid="icon-TextSearchIcon"]', {
timeout: 30000,
});

View file

@ -16,13 +16,11 @@ test.skip(
await page.getByTestId("fit_view").click();
let outdatedComponents = await page
.getByTestId("icon-AlertTriangle")
.count();
let outdatedComponents = await page.getByTestId("update-button").count();
while (outdatedComponents > 0) {
await page.getByTestId("icon-AlertTriangle").first().click();
outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
await page.getByTestId("update-button").first().click();
outdatedComponents = await page.getByTestId("update-button").count();
}
await page

View file

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
test("user must be able to move flow from folder", async ({ page }) => {
const randomName = Math.random().toString(36).substring(2, 15);
@ -9,17 +10,7 @@ test("user must be able to move flow from folder", async ({ page }) => {
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="input-flow-name"]', {
timeout: 3000,
});
await page.getByTestId("flow_menu_trigger").click();
await page.getByText("Edit Details").first().click();
await page.getByPlaceholder("Flow name").fill(randomName);
await page.getByTestId("save-flow-settings").click();
await page.getByText("Changes saved successfully").isVisible();
await renameFlow(page, { flowName: randomName });
await page.getByTestId("icon-ChevronLeft").click();
await page.waitForSelector('[data-testid="add-project-button"]', {

View file

@ -1,5 +1,6 @@
import { expect, Page, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { renameFlow } from "../../utils/rename-flow";
async function verifyTextareaValue(
page: Page,
@ -55,11 +56,7 @@ test(
state: "visible",
});
await page.getByTestId("input-flow-name").click();
await page.getByTestId("input-flow-name").fill(randomFlowName);
await page.keyboard.press("Enter");
await renameFlow(page, { flowName: randomFlowName });
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");

View file

@ -37,9 +37,6 @@ test(
await uploadFile(page, "chain.png");
await page.getByTestId("button_run_chat output").click();
await page.getByText("built successfully").last().click({
timeout: 15000,
});
await page.getByRole("button", { name: "Playground", exact: true }).click();

View file

@ -24,10 +24,6 @@ test(
await page.getByTestId("button_run_chat output").click();
await page.waitForSelector("text=built successfully", { timeout: 30000 });
await page.getByText("built successfully").last().click({
timeout: 30000,
});
await page.getByTestId("user-profile-settings").click();
await page.waitForSelector('text="Settings"');

View file

@ -0,0 +1,50 @@
import { Page } from "playwright/test";
export const renameFlow = async (
page: Page,
{
flowName,
flowDescription,
}: { flowName?: string; flowDescription?: string } = {},
) => {
await page.getByTestId("flow_name").isVisible({ timeout: 3000 });
await page.getByTestId("flow_name").click({ timeout: 3000 });
await page.waitForTimeout(500);
await page.getByTestId("input-flow-name").click({ timeout: 3000 });
const flowNameInput = await page.getByTestId("input-flow-name").inputValue();
if (flowName) {
await page.getByTestId("input-flow-name").fill(flowName);
}
const flowDescriptionInput = await page
.getByTestId("input-flow-description")
.inputValue();
if (flowDescription) {
await page.getByTestId("input-flow-description").fill(flowDescription);
}
if (flowName || flowDescription) {
await page.getByTestId("save-flow-settings").isEnabled({ timeout: 3000 });
await page.getByTestId("save-flow-settings").click();
await page
.getByText("Changes saved successfully")
.last()
.isVisible({ timeout: 3000 });
await page.getByText("Changes saved successfully").last().click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 30000,
});
} else {
await page.getByTestId("save-flow-settings").isDisabled({ timeout: 3000 });
await page.getByTestId("cancel-flow-settings").isEnabled({ timeout: 3000 });
await page.getByTestId("cancel-flow-settings").click();
}
return {
flowName: flowNameInput,
flowDescription: flowDescriptionInput,
};
};