feat: Add OutputModal component for displaying output visualization
The new OutputModal component is added to the genericNode module. It provides a modal window for displaying the output visualization. The component includes a header, content, and footer sections. The content section includes the SwitchOutputView component, which renders different output views based on the type of results. The footer section includes an "Ok" button for closing the modal.
This commit is contained in:
parent
3194ba7aee
commit
391ed6a001
3 changed files with 226 additions and 314 deletions
|
|
@ -0,0 +1,38 @@
|
|||
import RecordsOutputComponent from "../../../../../../components/recordsOutputComponent";
|
||||
import { Case } from "../../../../../../shared/components/caseComponent";
|
||||
import TextOutputView from "../../../../../../shared/components/textOutputView";
|
||||
import useFlowStore from "../../../../../../stores/flowStore";
|
||||
|
||||
export default function SwitchOutputView(nodeId): JSX.Element {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node?.id === nodeId?.nodeId);
|
||||
|
||||
const flowPoolNode = (flowPool[node!.id] ?? [])[
|
||||
(flowPool[node!.id]?.length ?? 1) - 1
|
||||
];
|
||||
|
||||
const results = flowPoolNode?.data?.logs[0] ?? "";
|
||||
|
||||
const checkType = () => {
|
||||
const typeOutput = typeof results;
|
||||
return typeOutput;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Case condition={!flowPoolNode}>
|
||||
<div>NO OUTPUT</div>
|
||||
</Case>
|
||||
|
||||
<Case condition={node && checkType() === "object"}>
|
||||
<TextOutputView left={false} flowPool={flowPool} node={node} />
|
||||
</Case>
|
||||
|
||||
<Case condition={node && Array.isArray(results)}>
|
||||
<RecordsOutputComponent flowPool={flowPoolNode} pagination={true} />
|
||||
</Case>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Button } from "../../../../components/ui/button";
|
||||
import BaseModal from "../../../../modals/baseModal";
|
||||
import SwitchOutputView from "./components/switchOutputView";
|
||||
|
||||
export default function OutputModal({ open, setOpen, nodeId }): JSX.Element {
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen} size="medium-h-full">
|
||||
<BaseModal.Header description="Output Visualization">
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2">Output View</span>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<SwitchOutputView nodeId={nodeId} />
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<div className="flex w-full justify-end pt-2">
|
||||
<Button className="flex gap-2 px-3" onClick={() => setOpen(false)}>
|
||||
Ok
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +1,11 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import {
|
||||
NATIVE_CATEGORIES,
|
||||
RUN_TIMESTAMP_PREFIX,
|
||||
STATUS_BUILD,
|
||||
STATUS_BUILDING,
|
||||
|
|
@ -22,26 +17,33 @@ import { useDarkStore } from "../../stores/darkStore";
|
|||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { APIClassType, VertexBuildTypeAPI } from "../../types/api";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import { countHandlesFn } from "../helpers/count-handles";
|
||||
import { getSpecificClassFromBuildStatus } from "../helpers/get-class-from-build-status";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useIconNodeRender from "../hooks/use-icon-render";
|
||||
import useIconStatus from "../hooks/use-icons-status";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
import useUpdateValidationStatus from "../hooks/use-update-validation-status";
|
||||
import useValidationStatusString from "../hooks/use-validation-status-string";
|
||||
import getFieldTitle from "../utils/get-field-title";
|
||||
import sortFields from "../utils/sort-fields";
|
||||
import OutputModal from "./components/outputModal";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
xPos,
|
||||
yPos,
|
||||
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
xPos?: number;
|
||||
yPos?: number;
|
||||
}): JSX.Element {
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
|
|
@ -51,203 +53,42 @@ export default function GenericNode({
|
|||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!,
|
||||
);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!,
|
||||
);
|
||||
const [openOutputModal, setOpenOutputModal] = useState(false);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
const [handles, setHandles] = useState<number>(0);
|
||||
|
||||
const [validationString, setValidationString] = useState<string>("");
|
||||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (
|
||||
!NATIVE_CATEGORIES.includes(types[data.type]) ||
|
||||
!data.node?.template?.code?.value
|
||||
)
|
||||
return;
|
||||
const thisNodeTemplate = templates[data.type].template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const componentsToIgnore = ["Custom Component", "Prompt"];
|
||||
if (
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnore.includes(data.node!.display_name)
|
||||
) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value]);
|
||||
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated],
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
function countHandles(): void {
|
||||
let count = Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateCamp) => {
|
||||
const { template } = data.node!;
|
||||
if (template[templateCamp].input_types) return true;
|
||||
if (!template[templateCamp].show) return false;
|
||||
switch (template[templateCamp].type) {
|
||||
case "str":
|
||||
case "bool":
|
||||
case "float":
|
||||
case "code":
|
||||
case "prompt":
|
||||
case "file":
|
||||
case "int":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||
|
||||
setHandles(count);
|
||||
}
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const iconStatus = useIconStatus(buildStatus, validationStatus);
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
// State for outline color
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
// should be empty string if no duration
|
||||
// else should be `Duration: ${duration}`
|
||||
const getDurationString = (duration: number | undefined|string): string => {
|
||||
if (duration === undefined) {
|
||||
return "";
|
||||
} else {
|
||||
return `${duration}`;
|
||||
}
|
||||
};
|
||||
const durationString = getDurationString(validationStatus?.data.duration);
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
setNode,
|
||||
setIsOutdated,
|
||||
updateNodeInternals,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
const relevantData =
|
||||
flowPool[data.id] && flowPool[data.id]?.length > 0
|
||||
? flowPool[data.id][flowPool[data.id].length - 1]
|
||||
: null;
|
||||
if (relevantData) {
|
||||
console.log(relevantData)
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool[data.id], data.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (validationStatus?.data.logs) {
|
||||
// if it is not a string turn it into a string
|
||||
let newValidationString = "";
|
||||
if (Array.isArray(validationStatus.data.logs)) {
|
||||
newValidationString = validationStatus.data.logs
|
||||
.map((log) => log.message)
|
||||
.join("\n");
|
||||
}
|
||||
if (typeof newValidationString !== "string") {
|
||||
newValidationString = JSON.stringify(validationStatus.data.logs);
|
||||
}
|
||||
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.data.logs]);
|
||||
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const nameEditable = true;
|
||||
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
const isEmoji = emojiRegex.test(data?.node?.icon!);
|
||||
|
||||
const iconNodeRender = useCallback(() => {
|
||||
const iconElement = data?.node?.icon;
|
||||
const iconColor = nodeColors[types[data.type]];
|
||||
const iconName =
|
||||
iconElement || (data.node?.flow ? "group_components" : name);
|
||||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||
}`;
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||
}
|
||||
}, [data, isEmoji, name, showNode]);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
|
||||
const nodeIconFragment = (icon) => {
|
||||
return <span className="text-lg">{icon}</span>;
|
||||
|
|
@ -263,79 +104,24 @@ export default function GenericNode({
|
|||
);
|
||||
};
|
||||
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const renderIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null
|
||||
) => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : 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"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
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";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
const renderIconStatus = () => {
|
||||
return (
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{iconStatus}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getNodeBorderClassName = (
|
||||
selected: boolean,
|
||||
showNode: boolean,
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus,
|
||||
isDark,
|
||||
);
|
||||
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
|
|
@ -354,6 +140,65 @@ export default function GenericNode({
|
|||
const getNodeSizeClass = (showNode) =>
|
||||
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
|
||||
|
||||
const nameEditable = true;
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
const isEmoji = emojiRegex.test(data?.node?.icon!);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, types);
|
||||
useValidationStatusString(validationStatus, setValidationString);
|
||||
useUpdateValidationStatus(data?.id, flowPool, setValidationStatus);
|
||||
|
||||
const iconNodeRender = useIconNodeRender(
|
||||
data,
|
||||
types,
|
||||
nodeColors,
|
||||
name,
|
||||
showNode,
|
||||
isEmoji,
|
||||
nodeIconFragment,
|
||||
checkNodeIconFragment,
|
||||
);
|
||||
|
||||
function countHandles(): void {
|
||||
const count = countHandlesFn(data);
|
||||
setHandles(count);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return (
|
||||
<NodeToolbar>
|
||||
|
|
@ -603,67 +448,63 @@ export default function GenericNode({
|
|||
)}
|
||||
</div>
|
||||
{showNode && (
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="mb-3 ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="mb-2 mt-2 flex justify-center font-semibold text-muted-foreground">
|
||||
Output
|
||||
</span>
|
||||
<div className="max-h-96 overflow-auto font-normal custom-scroll">
|
||||
{validationString.split("\n").map((line, index) => (
|
||||
<div className="font-normal" key={index}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
setOpenOutputModal(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
</div>
|
||||
</div>
|
||||
STATUS##
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
>
|
||||
{renderIconStatus()}
|
||||
</div>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -865,6 +706,14 @@ export default function GenericNode({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openOutputModal && (
|
||||
<OutputModal
|
||||
open={openOutputModal}
|
||||
nodeId={data.id}
|
||||
setOpen={setOpenOutputModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue