From f76c052ed1e459dcc8e05a5cd184e29038a879f9 Mon Sep 17 00:00:00 2001 From: Nathan Vieira Marcelino Date: Tue, 24 Oct 2023 09:00:45 -0300 Subject: [PATCH 1/4] fix: icons styles and result styles --- frontend/src/components/WorkflowPanel/DefaultNode/index.tsx | 4 ++-- frontend/src/components/WorkflowPanel/RunNode/index.tsx | 2 +- .../components/WorkflowDetail/CustomTabMenu/TaskResult.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/WorkflowPanel/DefaultNode/index.tsx b/frontend/src/components/WorkflowPanel/DefaultNode/index.tsx index 6f351d66..e30c310a 100644 --- a/frontend/src/components/WorkflowPanel/DefaultNode/index.tsx +++ b/frontend/src/components/WorkflowPanel/DefaultNode/index.tsx @@ -71,7 +71,7 @@ export const CustomNode = memo(({ id, data, selected }) => { ...data.style.nodeStyle, display: "flex", flexDirection: "row", - justifyContent: "center", + justifyContent: "space-evenly", alignItems: "center", position: "relative", @@ -88,7 +88,7 @@ export const CustomNode = memo(({ id, data, selected }) => { backgroundColor: theme.palette.error.light, color: theme.palette.error.contrastText, }), - }; + } satisfies CSSProperties; }, [data, selected]); const { sourcePosition, targetPosition } = useMemo( diff --git a/frontend/src/components/WorkflowPanel/RunNode/index.tsx b/frontend/src/components/WorkflowPanel/RunNode/index.tsx index 6ee470b7..d3f844a4 100644 --- a/frontend/src/components/WorkflowPanel/RunNode/index.tsx +++ b/frontend/src/components/WorkflowPanel/RunNode/index.tsx @@ -88,7 +88,7 @@ const RunNode = memo(({ id, data, selected }) => { ...data.style.nodeStyle, display: "flex", flexDirection: "row", - justifyContent: "center", + justifyContent: "space-evenly", alignItems: "center", textAlign: "center", position: "relative", diff --git a/frontend/src/features/workflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx b/frontend/src/features/workflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx index 85f1f785..35314721 100644 --- a/frontend/src/features/workflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx +++ b/frontend/src/features/workflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx @@ -14,6 +14,7 @@ export const TaskResult = (props: ITaskResultProps) => { const style: CSSProperties = { height: "100%", + width: "100%", overflowY: "scroll", overflowX: "hidden", wordWrap: "break-word", From 1c2d2c30fdffdfcf30ce9c90a86d3e44eff1f900 Mon Sep 17 00:00:00 2001 From: Nathan Vieira Marcelino Date: Tue, 24 Oct 2023 11:23:42 -0300 Subject: [PATCH 2/4] feat: export workflow --- .../components/WorkflowEditor.tsx | 108 +++++++- .../context/workflowPiecesData.tsx | 2 +- .../context/workflowsEditor.tsx | 262 ++++++++++-------- frontend/src/utils/downloadJson.ts | 26 ++ frontend/src/utils/index.ts | 1 + frontend/tsconfig.json | 2 +- 6 files changed, 278 insertions(+), 123 deletions(-) create mode 100644 frontend/src/utils/downloadJson.ts diff --git a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx index 58c21f8e..94855076 100644 --- a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx +++ b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx @@ -1,8 +1,9 @@ import { Settings as SettingsSuggestIcon } from "@mui/icons-material"; import ClearIcon from "@mui/icons-material/Clear"; import DownloadIcon from "@mui/icons-material/Download"; +import IosShareIcon from "@mui/icons-material/IosShare"; import SaveIcon from "@mui/icons-material/Save"; -import { Button, Grid, Paper } from "@mui/material"; +import { Button, Grid, Paper, styled } from "@mui/material"; import { AxiosError } from "axios"; import Loading from "components/Loading"; import { @@ -15,11 +16,12 @@ import { useWorkflowsEditor } from "features/workflowEditor/context"; import { type DragEvent, useCallback, useRef, useState } from "react"; import { toast } from "react-toastify"; import { type Edge, type Node, type XYPosition } from "reactflow"; -import { yupResolver, useInterval } from "utils"; +import { yupResolver, useInterval, exportToJson } from "utils"; import { v4 as uuidv4 } from "uuid"; import * as yup from "yup"; import { type IWorkflowPieceData, storageAccessModes } from "../context/types"; +import { type DominoWorkflowForage } from "../context/workflowsEditor"; import { containerResourcesSchema } from "../schemas/containerResourcesSchemas"; import { extractDefaultInputValues, extractDefaultValues } from "../utils"; @@ -42,6 +44,18 @@ const getId = (module_name: string) => { return `${module_name}_${uuidv4()}`; }; +const VisuallyHiddenInput = styled("input")({ + clip: "rect(0 0 0 0)", + clipPath: "inset(50%)", + height: 1, + overflow: "hidden", + position: "absolute", + bottom: 0, + left: 0, + whiteSpace: "nowrap", + width: 1, +}); + export const WorkflowsEditorComponent: React.FC = () => { const workflowPanelRef = useRef(null); const [sidebarSettingsDrawer, setSidebarSettingsDrawer] = useState(false); @@ -70,7 +84,7 @@ export const WorkflowsEditorComponent: React.FC = () => { const { clearForageData, - workflowsEditorBodyFromFlowchart, + generateWorkflowsEditorBodyParams, fetchWorkflowForage, handleCreateWorkflow, fetchForagePieceById, @@ -153,7 +167,7 @@ export const WorkflowsEditorComponent: React.FC = () => { await validateWorkflowPiecesData(payload); await validateWorkflowSettings(payload); - const data = await workflowsEditorBodyFromFlowchart(); + const data = await generateWorkflowsEditorBodyParams(payload); await handleCreateWorkflow({ workspace_id: workspace?.id, ...data }); @@ -175,7 +189,7 @@ export const WorkflowsEditorComponent: React.FC = () => { handleCreateWorkflow, validateWorkflowPiecesData, validateWorkflowSettings, - workflowsEditorBodyFromFlowchart, + generateWorkflowsEditorBodyParams, workspace?.id, ]); @@ -185,6 +199,76 @@ export const WorkflowsEditorComponent: React.FC = () => { workflowPanelRef.current?.setNodes([]); }, [clearForageData]); + const handleExport = useCallback(async () => { + await saveDataToLocalForage(); + const payload = await fetchWorkflowForage(); + exportToJson(payload, payload.workflowSettingsData?.config?.name); + }, []); + + const validateJsonImported = useCallback( + async (json: DominoWorkflowForage) => { + const getRepositories = function ( + workflowPieces: DominoWorkflowForage["workflowPieces"], + ) { + return [ + ...new Set( + Object.values(workflowPieces) + .reduce>((acc, next) => { + acc.push(next.source_image); + return acc; + }, []) + .filter((su) => !!su) as string[], + ), + ]; + }; + + const { workflowPieces } = await fetchWorkflowForage(); + + const currentRepositories = getRepositories(workflowPieces); + const incomeRepositories = getRepositories(json.workflowPieces); + const differences = currentRepositories.filter( + (x) => !incomeRepositories.includes(x), + ); + + return differences.length ? differences : null; + }, + [fetchWorkflowForage], + ); + + const handleImport = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + try { + const jsonData = JSON.parse( + e.target?.result as string, + ) as DominoWorkflowForage; + + validateJsonImported(jsonData) + .then((diferences) => { + if (diferences) { + alert( + `Missing some repositories: ${JSON.stringify(diferences)}`, + ); + } else { + alert("Same itens"); + } + }) + .catch((e) => { + alert(e); + }); + } catch (error) { + console.error("Error parsing JSON file:", error); + } + }; + + reader.readAsText(file); + } + }, []); + const onNodesDelete = useCallback( async (nodes: any) => { for (const node of nodes) { @@ -344,11 +428,23 @@ export const WorkflowsEditorComponent: React.FC = () => { + + + + + + )} + + + + + + + ); + }, +); + +Modal.displayName = "Modal"; diff --git a/frontend/src/components/NewFeatureDialog/index.tsx b/frontend/src/components/NewFeatureDialog/index.tsx deleted file mode 100644 index 449507f7..00000000 --- a/frontend/src/components/NewFeatureDialog/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, -} from "@mui/material"; -import React from "react"; - -interface Props { - isOpen: boolean; - confirmFn: () => void; -} - -export const NewFeatureDialog: React.FC = ({ isOpen, confirmFn }) => { - return ( - - New feature - - - This feature is not ready yet! We launch new versions every time, - check out our changelog for more information ! - - - - - - - - - - - ); -}; diff --git a/frontend/src/features/workflowEditor/components/SidebarForm/index.tsx b/frontend/src/features/workflowEditor/components/SidebarForm/index.tsx index 936a57f5..dae41c48 100644 --- a/frontend/src/features/workflowEditor/components/SidebarForm/index.tsx +++ b/frontend/src/features/workflowEditor/components/SidebarForm/index.tsx @@ -33,7 +33,7 @@ const SidebarPieceForm: React.FC = (props) => { const { schema, formId, open, onClose, title } = props; const { - setForageWorkflowPiecesData, + setForageWorkflowPiecesDataById, fetchForageWorkflowPiecesDataById, setForageWorkflowPiecesOutputSchema, clearDownstreamDataById, @@ -141,10 +141,10 @@ const SidebarPieceForm: React.FC = (props) => { const saveData = useCallback(async () => { if (formId && open) { - await setForageWorkflowPiecesData(formId, data as IWorkflowPieceData); + await setForageWorkflowPiecesDataById(formId, data as IWorkflowPieceData); await updateOutputSchema(); } - }, [formId, open, setForageWorkflowPiecesData, data, updateOutputSchema]); + }, [formId, open, setForageWorkflowPiecesDataById, data, updateOutputSchema]); // load forage useEffect(() => { diff --git a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx index 94855076..599dffc6 100644 --- a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx +++ b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx @@ -6,6 +6,7 @@ import SaveIcon from "@mui/icons-material/Save"; import { Button, Grid, Paper, styled } from "@mui/material"; import { AxiosError } from "axios"; import Loading from "components/Loading"; +import { Modal, type ModalRef } from "components/Modal"; import { type WorkflowPanelRef, WorkflowPanel, @@ -16,6 +17,7 @@ import { useWorkflowsEditor } from "features/workflowEditor/context"; import { type DragEvent, useCallback, useRef, useState } from "react"; import { toast } from "react-toastify"; import { type Edge, type Node, type XYPosition } from "reactflow"; +import localForage from "services/config/localForage.config"; import { yupResolver, useInterval, exportToJson } from "utils"; import { v4 as uuidv4 } from "uuid"; import * as yup from "yup"; @@ -69,6 +71,9 @@ export const WorkflowsEditorComponent: React.FC = () => { "horizontal", ); + const incompatiblePiecesModalRef = useRef(null); + const [incompatiblesPieces, setIncompatiblesPieces] = useState([]); + const { workspace } = useWorkspaces(); const saveDataToLocalForage = useCallback(async () => { @@ -95,7 +100,8 @@ export const WorkflowsEditorComponent: React.FC = () => { removeForageWorkflowPiecesById, removeForageWorkflowPieceDataById, fetchWorkflowPieceById, - setForageWorkflowPiecesData, + setForageWorkflowPiecesDataById, + importWorkflowToForage, clearDownstreamDataById, setWorkflowEdges, setWorkflowNodes, @@ -222,12 +228,17 @@ export const WorkflowsEditorComponent: React.FC = () => { ]; }; - const { workflowPieces } = await fetchWorkflowForage(); - - const currentRepositories = getRepositories(workflowPieces); + const currentRepositories = [ + ...new Set( + Object.values((await localForage.getItem("pieces")) as any)?.map( + (p: any) => p?.source_image, + ), + ), + ]; const incomeRepositories = getRepositories(json.workflowPieces); - const differences = currentRepositories.filter( - (x) => !incomeRepositories.includes(x), + + const differences = incomeRepositories.filter( + (x) => !currentRepositories.includes(x), ); return differences.length ? differences : null; @@ -235,39 +246,53 @@ export const WorkflowsEditorComponent: React.FC = () => { [fetchWorkflowForage], ); - const handleImport = useCallback((e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - - if (file) { - const reader = new FileReader(); - - reader.onload = (e) => { - try { - const jsonData = JSON.parse( - e.target?.result as string, - ) as DominoWorkflowForage; - - validateJsonImported(jsonData) - .then((diferences) => { - if (diferences) { - alert( - `Missing some repositories: ${JSON.stringify(diferences)}`, - ); - } else { - alert("Same itens"); - } - }) - .catch((e) => { - alert(e); - }); - } catch (error) { - console.error("Error parsing JSON file:", error); - } - }; - - reader.readAsText(file); - } - }, []); + const handleImport = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + try { + const jsonData = JSON.parse( + e.target?.result as string, + ) as DominoWorkflowForage; + + validateJsonImported(jsonData) + .then((diferences) => { + if (diferences) { + toast.error( + "Some repositories are missing or incompatible version", + ); + setIncompatiblesPieces(diferences); + incompatiblePiecesModalRef.current?.open(); + return; + } + + workflowPanelRef?.current?.setNodes(jsonData.workflowNodes); + workflowPanelRef?.current?.setEdges(jsonData.workflowEdges); + void importWorkflowToForage(jsonData); + }) + .catch((e) => { + console.log(e); + }); + } catch (error) { + console.error("Error parsing JSON file:", error); + } + }; + + reader.readAsText(file); + } + }, + [ + validateJsonImported, + workflowPanelRef, + importWorkflowToForage, + setIncompatiblesPieces, + incompatiblePiecesModalRef, + ], + ); const onNodesDelete = useCallback( async (nodes: any) => { @@ -351,7 +376,10 @@ export const WorkflowsEditorComponent: React.FC = () => { inputs: defaultInputs, }; - await setForageWorkflowPiecesData(newNode.id, defaultWorkflowPieceData); + await setForageWorkflowPiecesDataById( + newNode.id, + defaultWorkflowPieceData, + ); return newNode; }, [ @@ -359,7 +387,7 @@ export const WorkflowsEditorComponent: React.FC = () => { fetchForagePieceById, setForageWorkflowPieces, getForageWorkflowPieces, - setForageWorkflowPiecesData, + setForageWorkflowPiecesDataById, ], ); @@ -442,6 +470,17 @@ export const WorkflowsEditorComponent: React.FC = () => { > Import + + {incompatiblesPieces.map((item) => ( +
  • {item}
  • + ))} + + } + ref={incompatiblePiecesModalRef} + /> diff --git a/frontend/src/features/workflowEditor/context/workflowPiecesData.tsx b/frontend/src/features/workflowEditor/context/workflowPiecesData.tsx index c3e7e035..578ab218 100644 --- a/frontend/src/features/workflowEditor/context/workflowPiecesData.tsx +++ b/frontend/src/features/workflowEditor/context/workflowPiecesData.tsx @@ -7,10 +7,11 @@ import { type IWorkflowPieceData } from "./types"; export type ForagePiecesData = Record; export interface IWorkflowPiecesDataContext { - setForageWorkflowPiecesData: ( + setForageWorkflowPiecesDataById: ( id: string, pieceData: IWorkflowPieceData, ) => Promise; + setForageWorkflowPiecesData: (pieceData: ForagePiecesData) => Promise; fetchForageWorkflowPiecesData: () => Promise; fetchForageWorkflowPiecesDataById: ( id: string, @@ -26,7 +27,7 @@ export const [WorkflowPiecesDataContext, useWorkflowPiecesData] = const WorkflowPiecesDataProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const setForageWorkflowPiecesData = useCallback( + const setForageWorkflowPiecesDataById = useCallback( async (id: string, pieceData: IWorkflowPieceData) => { let currentData = await localForage.getItem("workflowPiecesData"); @@ -38,6 +39,12 @@ const WorkflowPiecesDataProvider: React.FC<{ children: React.ReactNode }> = ({ }, [], ); + const setForageWorkflowPiecesData = useCallback( + async (pieceData: ForagePiecesData) => { + await localForage.setItem("workflowPiecesData", pieceData); + }, + [], + ); const fetchForageWorkflowPiecesData = useCallback(async () => { const workflowPiecesData = @@ -120,6 +127,7 @@ const WorkflowPiecesDataProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const value: IWorkflowPiecesDataContext = { + setForageWorkflowPiecesDataById, setForageWorkflowPiecesData, fetchForageWorkflowPiecesData, fetchForageWorkflowPiecesDataById, diff --git a/frontend/src/features/workflowEditor/context/workflowsEditor.tsx b/frontend/src/features/workflowEditor/context/workflowsEditor.tsx index 02e2f8a0..bc87266e 100644 --- a/frontend/src/features/workflowEditor/context/workflowsEditor.tsx +++ b/frontend/src/features/workflowEditor/context/workflowsEditor.tsx @@ -50,10 +50,13 @@ interface IWorkflowsEditorContext IWorkflowSettingsContext, IWorkflowPieceContext, IWorkflowPiecesDataContext { - fetchWorkflowForage: () => Promise; // TODO add type + fetchWorkflowForage: () => Promise; + importWorkflowToForage: ( + importedWorkflow: DominoWorkflowForage, + ) => Promise; generateWorkflowsEditorBodyParams: ( p: GenerateWorkflowsParams, - ) => Promise; // TODO add type + ) => Promise; handleCreateWorkflow: ( params: IPostWorkflowParams, ) => Promise; @@ -101,6 +104,7 @@ const WorkflowsEditorProvider: FC<{ children?: React.ReactNode }> = ({ fetchForageWorkflowPiecesData, fetchForageWorkflowPiecesDataById, setForageWorkflowPiecesData, + setForageWorkflowPiecesDataById, clearForageWorkflowPiecesData, removeForageWorkflowPieceDataById, clearDownstreamDataById, @@ -142,6 +146,23 @@ const WorkflowsEditorProvider: FC<{ children?: React.ReactNode }> = ({ getForageWorkflowPieces, ]); + const importWorkflowToForage = useCallback( + async (dominoWorkflow: DominoWorkflowForage) => { + await setForageWorkflowPieces(dominoWorkflow.workflowPieces); + await setForageWorkflowPiecesData(dominoWorkflow.workflowPiecesData); + await setWorkflowSettingsData(dominoWorkflow.workflowSettingsData); + await setWorkflowNodes(dominoWorkflow.workflowNodes); + await setWorkflowEdges(dominoWorkflow.workflowEdges); + }, + [ + setForageWorkflowPieces, + setForageWorkflowPiecesData, + setWorkflowSettingsData, + setWorkflowNodes, + setWorkflowEdges, + ], + ); + const generateWorkflowsEditorBodyParams = useCallback( async ({ workflowPiecesData, @@ -303,6 +324,8 @@ const WorkflowsEditorProvider: FC<{ children?: React.ReactNode }> = ({ search, handleSearch, + importWorkflowToForage, + setWorkflowEdges, setWorkflowNodes, fetchForageWorkflowEdges, @@ -317,6 +340,7 @@ const WorkflowsEditorProvider: FC<{ children?: React.ReactNode }> = ({ clearForageWorkflowPieces, setForageWorkflowPiecesData, + setForageWorkflowPiecesDataById, fetchForageWorkflowPiecesData, fetchForageWorkflowPiecesDataById, removeForageWorkflowPieceDataById, diff --git a/frontend/src/features/workflows/components/WorkflowsList/Actions.tsx b/frontend/src/features/workflows/components/WorkflowsList/Actions.tsx index 830f2ce5..883e01b4 100644 --- a/frontend/src/features/workflows/components/WorkflowsList/Actions.tsx +++ b/frontend/src/features/workflows/components/WorkflowsList/Actions.tsx @@ -3,10 +3,10 @@ import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"; import { IconButton } from "@mui/material"; import { type CommonProps } from "@mui/material/OverridableComponent"; -import { NewFeatureDialog } from "components/NewFeatureDialog"; +import { Modal, type ModalRef } from "components/Modal"; import { type IWorkflow } from "features/workflows/types"; import theme from "providers/theme.config"; -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import { ConfirmDeleteModal } from "./ConfirmDeleteModal"; @@ -19,7 +19,7 @@ interface Props extends CommonProps { export const Actions: React.FC = ({ runFn, deleteFn, className }) => { const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [newFeatureModal, setNewFeatureModal] = useState(false); + const newFeatureModal = useRef(null); return ( <> @@ -31,7 +31,7 @@ export const Actions: React.FC = ({ runFn, deleteFn, className }) => { { - setNewFeatureModal(true); + newFeatureModal.current?.open(); }} > = ({ runFn, deleteFn, className }) => { style={{ pointerEvents: "none", color: theme.palette.error.main }} /> - { - setNewFeatureModal(false); - }} + { ); const handleRowClick = useCallback>( - (row, event) => { + (params: GridRowParams, event) => { const isActionButtonClick = event.target instanceof Element && event.target.classList.contains(".action-button"); if (!isActionButtonClick) { - // Handle row click logic only if it's not an action button click - navigate(`/workflows/${row.id}`); + if (params.row.status !== "failed" && params.row.status !== "creating") + navigate(`/workflows/${params.id}`); } }, [navigate], @@ -166,6 +167,9 @@ export const WorkflowList: React.FC = () => { density="comfortable" columns={columns} rows={rows} + isRowSelectable={(params) => + params.row.status !== "failed" && params.row.status !== "creating" + } onRowClick={handleRowClick} pagination paginationMode="server" From 017f8bd68a36bb8cfe0163d66cee9024469e3a2e Mon Sep 17 00:00:00 2001 From: Nathan Vieira Marcelino Date: Fri, 27 Oct 2023 12:54:38 -0300 Subject: [PATCH 4/4] fix: clear state to trigger onChange if import same file --- .../workflowEditor/components/WorkflowEditor.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx index 599dffc6..c9e90e3f 100644 --- a/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx +++ b/frontend/src/features/workflowEditor/components/WorkflowEditor.tsx @@ -246,6 +246,8 @@ export const WorkflowsEditorComponent: React.FC = () => { [fetchWorkflowForage], ); + const fileInputRef = useRef(null); + const handleImport = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -277,6 +279,10 @@ export const WorkflowsEditorComponent: React.FC = () => { .catch((e) => { console.log(e); }); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } } catch (error) { console.error("Error parsing JSON file:", error); } @@ -291,6 +297,7 @@ export const WorkflowsEditorComponent: React.FC = () => { importWorkflowToForage, setIncompatiblesPieces, incompatiblePiecesModalRef, + fileInputRef, ], ); @@ -469,7 +476,11 @@ export const WorkflowsEditorComponent: React.FC = () => { startIcon={} > Import - +