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:
parent
08b13df4a4
commit
984b172d5d
64 changed files with 1067 additions and 1155 deletions
|
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
116
src/frontend/src/components/core/flowSettingsComponent/index.tsx
Normal file
116
src/frontend/src/components/core/flowSettingsComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
};
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -231,7 +231,6 @@ const ListComponent = ({
|
|||
open={openSettings}
|
||||
setOpen={setOpenSettings}
|
||||
flowData={flowData}
|
||||
details
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.", {
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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"]', {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]', {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
|
|
|
|||
50
src/frontend/tests/utils/rename-flow.ts
Normal file
50
src/frontend/tests/utils/rename-flow.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue