Merge remote-tracking branch 'origin/zustand/io/migration' into globalVariables

This commit is contained in:
Lucas Oliveira 2024-03-22 16:21:02 +01:00
commit 0f867dc485
46 changed files with 1157 additions and 485 deletions

View file

@ -3,6 +3,7 @@ import "reactflow/dist/style.css";
import "./App.css";
import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
@ -47,6 +48,9 @@ export default function App() {
(state) => state.setGlobalVariables
);
const checkHasStore = useStoreStore((state) => state.checkHasStore);
const navigate = useNavigate();
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
useEffect(() => {
refreshStars();
@ -67,11 +71,12 @@ export default function App() {
}, [isAuthenticated]);
useEffect(() => {
checkApplicationHealth();
// Timer to call getHealth every 5 seconds
const timer = setInterval(() => {
getHealth()
.then(() => {
if (fetchError) setFetchError(false);
onHealthCheck();
})
.catch(() => {
setFetchError(true);
@ -84,6 +89,30 @@ export default function App() {
};
}, []);
const checkApplicationHealth = () => {
setIsLoadingHealth(true);
getHealth()
.then(() => {
onHealthCheck();
})
.catch(() => {
setFetchError(true);
});
setTimeout(() => {
setIsLoadingHealth(false);
}, 2000);
};
const onHealthCheck = () => {
setFetchError(false);
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
if (isLoading === true && window.location.pathname === "/") {
navigate("/flows");
window.location.reload();
}
};
return (
//need parent component with width and height
<div className="flex h-full flex-col">
@ -93,20 +122,29 @@ export default function App() {
}}
FallbackComponent={CrashErrorComponent}
>
{fetchError ? (
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
></FetchErrorComponent>
) : isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
<>
{
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
openModal={fetchError}
setRetry={() => {
checkApplicationHealth();
}}
isLoadingHealth={isLoadingHealth}
></FetchErrorComponent>
}
{isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</>
</ErrorBoundary>
<div></div>
<div className="app-div">

View file

@ -275,7 +275,8 @@ export default function GenericNode({
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
/>
) : validationStatus && !validationStatus.valid ? (
) : buildStatus === BuildStatus.ERROR ||
(validationStatus && !validationStatus.valid) ? (
<Xmark
isVisible={true}
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
@ -301,7 +302,10 @@ export default function GenericNode({
// INACTIVE should have its own class
return "inactive-status";
}
if (buildStatus === BuildStatus.BUILT && isInvalid) {
if (
(buildStatus === BuildStatus.BUILT && isInvalid) ||
buildStatus === BuildStatus.ERROR
) {
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
} else if (buildStatus === BuildStatus.BUILDING) {
return "building-status";

View file

@ -4,6 +4,7 @@ import Loading from "../../../components/ui/loading";
import { FlowType } from "../../../types/flow";
import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants";
import { BuildStatus } from "../../../constants/enums";
import useAlertStore from "../../../stores/alertStore";
import useFlowStore from "../../../stores/flowStore";
import { validateNodes } from "../../../utils/reactflowUtils";
@ -22,6 +23,7 @@ export default function BuildTrigger({
const buildFlow = useFlowStore((state) => state.buildFlow);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
const setErrorData = useAlertStore((state) => state.setErrorData);
const eventClick = isBuilding ? "pointer-events-none" : "";
@ -32,12 +34,15 @@ export default function BuildTrigger({
if (isBuilding) {
return;
}
const errors = validateNodes(nodes, edges);
const errorsObjs = validateNodes(nodes, edges);
const errors = errorsObjs.flatMap((errorObj) => errorObj.errors);
if (errors.length > 0) {
setErrorData({
title: MISSED_ERROR_ALERT,
list: errors,
});
const ids = errorsObjs.map((errorObj) => errorObj.id);
updateBuildStatus(ids, BuildStatus.ERROR);
return;
}
const minimumLoadingTime = 200; // in milliseconds

View file

@ -1,16 +1,51 @@
import BaseModal from "../../modals/baseModal";
import { fetchErrorComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function FetchErrorComponent({
message,
description,
openModal,
setRetry,
isLoadingHealth,
}: fetchErrorComponentType) {
return (
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">{description}</span>
</div>
<>
<BaseModal size="small-h-full" open={openModal} type="modal">
<BaseModal.Content>
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent
className={`h-16 w-16`}
name="Unplug"
></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">
{description}
</span>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="m-auto">
<Button
disabled={isLoadingHealth}
onClick={() => {
setRetry();
}}
>
{isLoadingHealth ? (
<div>
<IconComponent name={"Loader2"} className={"animate-spin"} />
</div>
) : (
"Retry"
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
</>
);
}

View file

@ -26,6 +26,7 @@ export default function PromptAreaComponent({
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
id={id}
field_name={field_name}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}

View file

@ -0,0 +1,119 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "../../utils/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};

View file

@ -12,4 +12,5 @@ export enum BuildStatus {
TO_BUILD = "TO_BUILD",
BUILT = "BUILT",
INACTIVE = "INACTIVE",
ERROR = "ERROR",
}

View file

@ -9,6 +9,12 @@ import {
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import {
Dialog as Modal,
DialogContent as ModalContent,
} from "../../components/ui/dialog-with-no-close";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
@ -76,6 +82,7 @@ interface BaseModalProps {
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
type?: "modal" | "dialog";
}
function BaseModal({
open,
@ -83,6 +90,7 @@ function BaseModal({
children,
size = "large",
onChangeOpenModal,
type = "dialog",
}: BaseModalProps) {
const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header
@ -156,22 +164,43 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</DialogContent>
</Dialog>
<>
{type === "modal" ? (
<Modal open={open} onOpenChange={setOpen}>
{triggerChild}
<ModalContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</ModalContent>
</Modal>
) : (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</DialogContent>
</Dialog>
)}
</>
);
}

View file

@ -31,6 +31,7 @@ import {
CHAT_SECOND_INITIAL_TEXT,
LANGFLOW_CHAT_TITLE,
} from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import { getBuildStatus } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
@ -49,6 +50,7 @@ export default function FormModal({
}): JSX.Element {
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
const flowState = useFlowStore((state) => state.flowState);
const setFlowState = useFlowStore((state) => state.setFlowState);
const [chatValue, setChatValue] = useState(() => {
@ -388,7 +390,8 @@ export default function FormModal({
function sendMessage(): void {
let nodeValidationErrors = validateNodes(nodes, edges);
if (nodeValidationErrors.length === 0) {
const errors = nodeValidationErrors.flatMap((error) => error.errors);
if (errors.length === 0) {
setLockChat(true);
let inputs = flowState?.input_keys;
setChatValue("");
@ -412,8 +415,10 @@ export default function FormModal({
} else {
setErrorData({
title: INFO_MISSING_ALERT,
list: nodeValidationErrors,
list: errors,
});
const ids = nodeValidationErrors.map((error) => error.id);
updateBuildStatus(ids, BuildStatus.ERROR);
}
}
function clearChat(): void {

View file

@ -94,6 +94,49 @@ export default function Page({
const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null);
function handleGroupNode() {
takeSnapshot();
if (validateSelection(lastSelection!, edges).length === 0) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const clonedSelection = cloneDeep(lastSelection);
updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!);
const { newFlow, removedEdges } = generateFlow(
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
const newEdges = reconnectEdges(newGroupNode, removedEdges);
setNodes([
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) => selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges([
...clonedEdges.filter(
(oldEdge) =>
!clonedSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
),
...newEdges,
]);
} else {
setErrorData({
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
});
}
}
const setNode = useFlowStore((state) => state.setNode);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
@ -104,49 +147,7 @@ export default function Page({
event.key === "g"
) {
event.preventDefault();
takeSnapshot();
if (validateSelection(lastSelection!, edges).length === 0) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const clonedSelection = cloneDeep(lastSelection);
updateIds(
{ nodes: clonedNodes, edges: clonedEdges },
clonedSelection!
);
const { newFlow, removedEdges } = generateFlow(
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
const newEdges = reconnectEdges(newGroupNode, removedEdges);
setNodes([
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) => selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges([
...clonedEdges.filter(
(oldEdge) =>
!clonedSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
),
...newEdges,
]);
} else {
setErrorData({
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
});
}
handleGroupNode();
}
if (
(event.ctrlKey || event.metaKey) &&
@ -484,58 +485,7 @@ export default function Page({
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
takeSnapshot();
if (
validateSelection(lastSelection!, edges).length === 0
) {
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const clonedSelection = cloneDeep(lastSelection);
updateIds(
{ nodes: clonedNodes, edges: clonedEdges },
clonedSelection!
);
const { newFlow, removedEdges } = generateFlow(
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
const newEdges = reconnectEdges(
newGroupNode,
removedEdges
);
setNodes([
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges([
...clonedEdges.filter(
(oldEdge) =>
!clonedSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
),
...newEdges,
]);
} else {
setErrorData({
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
});
}
handleGroupNode();
}}
/>
</ReactFlow>

View file

@ -270,19 +270,10 @@ export default function NodeToolbarComponent({
selected &&
isGroup &&
(event.ctrlKey || event.metaKey) &&
event.key === "u"
event.key === "g"
) {
event.preventDefault();
takeSnapshot();
expandGroupNode(
data.id,
updateFlowPosition(position, data.node?.flow!),
data.node!.template,
nodes,
edges,
setNodes,
setEdges
);
handleSelectChange("ungroup");
}
if (
selected &&
@ -571,7 +562,7 @@ export default function NodeToolbarComponent({
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.43em]">U</span>
<span className="absolute right-2 top-[0.43em]">G</span>
</div>
</SelectItem>
)}

View file

@ -438,16 +438,20 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const setErrorData = useAlertStore.getState().setErrorData;
const setNoticeData = useAlertStore.getState().setNoticeData;
function validateSubgraph(nodes: string[]) {
const errors = validateNodes(
const errorsObjs = validateNodes(
get().nodes.filter((node) => nodes.includes(node.id)),
get().edges
);
const errors = errorsObjs.map((obj) => obj.errors).flat();
if (errors.length > 0) {
setErrorData({
title: MISSED_ERROR_ALERT,
list: errors,
});
get().setIsBuilding(false);
const ids = errorsObjs.map((obj) => obj.id).flat();
console.log("ids", ids);
get().updateBuildStatus(ids, BuildStatus.ERROR);
throw new Error("Invalid nodes");
}
}
@ -538,7 +542,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
.filter(Boolean) as string[];
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
},
validateNodes: validateSubgraph,
onValidateNodes: validateSubgraph,
});
get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding();

View file

@ -88,6 +88,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
}
})
.catch((e) => {
set({ isLoading: false });
useAlertStore.getState().setErrorData({
title: "Could not load flows from database",
});

View file

@ -27,10 +27,6 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
resolve();
})
.catch((error) => {
useAlertStore.getState().setErrorData({
title: "An error has occurred while fetching types.",
list: ["Please refresh the page."],
});
console.error("An error has occurred while fetching types.");
console.log(error);
reject();

View file

@ -672,6 +672,9 @@ export type ApiKey = {
export type fetchErrorComponentType = {
message: string;
description: string;
openModal?: boolean;
setRetry: () => void;
isLoadingHealth: boolean;
};
export type dropdownButtonPropsType = {

View file

@ -20,7 +20,7 @@ type BuildVerticesParams = {
onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
onBuildStart?: (idList: VertexLayerElementType[]) => void;
validateNodes?: (nodes: string[]) => void;
onValidateNodes?: (nodes: string[]) => void;
};
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
@ -114,7 +114,7 @@ export async function buildVertices({
onBuildComplete,
onBuildError,
onBuildStart,
validateNodes,
onValidateNodes,
}: BuildVerticesParams) {
let verticesBuild = useFlowStore.getState().verticesBuild;
// if startNodeId and stopNodeId are provided
@ -133,10 +133,10 @@ export async function buildVertices({
if (onGetOrderSuccess) onGetOrderSuccess();
if (validateNodes) {
if (onValidateNodes) {
try {
const nodes = useFlowStore.getState().nodes;
validateNodes(nodes.map((node) => node.id));
onValidateNodes(nodes.map((node) => node.id));
} catch (e) {
return;
}

View file

@ -313,27 +313,40 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
) {
if (hasDuplicateKeys(template[t].value))
errors.push(
`${type} (${getFieldTitle(
`${displayName || type} (${getFieldTitle(
template,
t
)}) contains duplicate keys with the same values.`
);
if (hasEmptyKey(template[t].value))
errors.push(
`${type} (${getFieldTitle(template, t)}) field must not be empty.`
`${displayName || type} (${getFieldTitle(
template,
t
)}) field must not be empty.`
);
}
return errors;
}, [] as string[]);
}
export function validateNodes(nodes: Node[], edges: Edge[]) {
export function validateNodes(
nodes: Node[],
edges: Edge[]
): // this returns an array of tuples with the node id and the errors
Array<{ id: string; errors: Array<string> }> {
if (nodes.length === 0) {
return [
"No nodes found in the flow. Please add at least one node to the flow.",
{
id: "",
errors: [
"No nodes found in the flow. Please add at least one node to the flow.",
],
},
];
}
return nodes.flatMap((n: NodeType) => validateNode(n, edges));
// validateNode(n, edges) returns an array of errors for the node
return nodes.map((n) => ({ id: n.id, errors: validateNode(n, edges) }));
}
export function updateEdges(edges: Edge[]) {