/* eslint-disable react/no-multi-comp */ import React, { useState, useEffect, useLayoutEffect, memo, useMemo, useRef } from "react"; import ReactDOM from "react-dom" import theme from "../theme.jsx"; import { useInterval } from "react-powerhooks"; import { makeStyles, } from "@mui/styles"; import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx" import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid"; import { useNavigate, Link, useParams } from "react-router-dom"; import { useBeforeunload } from "react-beforeunload" import ReactJson from "react-json-view-ssr"; import { NestedMenuItem } from 'mui-nested-menu'; import Markdown from "react-markdown"; //import { useAlert import { ToastContainer, toast } from "react-toastify" import { isMobile } from "react-device-detect" import aa from 'search-insights' import Drift from "react-driftjs"; import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx"; import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; import algoliasearch from 'algoliasearch/lite'; import { Zoom, Fade, Slide, Avatar, Popover, TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, SwipeableDrawer, Switch, Chip, Card, List, ListItem, ListItemText, ListItemAvatar, Badge, AvatarGroup, Autocomplete, Radio, ButtonGroup, } from "@mui/material"; import { Folder as FolderIcon, VerifiedUser as VerifiedUserIcon, CheckCircle as CheckCircleIcon, Insights as InsightsIcon, LibraryBooks as LibraryBooksIcon, OpenInNew as OpenInNewIcon, Undo as UndoIcon, GetApp as GetAppIcon, Search as SearchIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, DragIndicator as DragIndicatorIcon, Error as ErrorIcon, Warning as WarningIcon, ArrowLeft as ArrowLeftIcon, ArrowRight as ArrowRightIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, FormatListNumbered as FormatListNumberedIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon, AddComment as AddCommentIcon, Edit as EditIcon, Send as SendIcon, Restore as RestoreIcon, Preview as PreviewIcon, ContentCopy as ContentCopyIcon, Circle as CircleIcon, SquareFoot as SquareFootIcon, AutoFixHigh as AutoFixHighIcon, Polyline as PolylineIcon, QueryStats as QueryStatsIcon, AutoAwesome as AutoAwesomeIcon, Add as AddIcon, ErrorOutline as ErrorOutlineIcon, ArrowForward as ArrowForwardIcon, } from "@mui/icons-material"; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; //import * as cytoscape from "cytoscape"; import cytoscape from "cytoscape"; import edgehandles from "cytoscape-edgehandles"; import CytoscapeComponent from "react-cytoscapejs"; import Draggable from "react-draggable"; import cytoscapestyle from "../defaultCytoscapeStyle.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { validateJson, collapseField, GetIconInfo } from "../views/Workflows.jsx"; import { GetParsedPaths, internalIds, } from "../views/Apps.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; import ParsedAction from "../components/ParsedAction.jsx"; import PaperComponent from "../components/PaperComponent.jsx" import ExtraApps from "../components/ExtraApps.jsx" import EditWorkflow from "../components/EditWorkflow.jsx" import { act } from "react"; // import AppStats from "../components/AppStats.jsx"; const noImage = "/public/no_image.png"; cytoscape.use(edgehandles); export const triggers = [ { name: "Webhook", type: "TRIGGER", status: "uninitialized", trigger_type: "WEBHOOK", errors: null, large_image: "data:image/png;base64,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", is_valid: true, label: "Webhook", environment: "onprem", description: "Custom HTTP input trigger", long_description: "Execute a workflow with an unauthicated POST request", id: "", }, { name: "Schedule", type: "TRIGGER", status: "uninitialized", trigger_type: "SCHEDULE", errors: null, large_image: "data:image/png;base64,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", label: "Schedule", is_valid: true, environment: "onprem", description: "Schedule time trigger", long_description: "Create a schedule based on cron", id: "", }, { name: "Pipelines", type: "TRIGGER", status: "uninitialized", description: "Run a pipeline trigger", trigger_type: "PIPELINE", errors: null, is_valid: true, label: "Pipeline", environment: "onprem", large_image: "/images/workflows/tenzir2.png", long_description: "Controls a pipeline to run things", id: "", }, { name: "Shuffle Workflow", type: "TRIGGER", status: "uninitialized", trigger_type: "SUBFLOW", errors: null, large_image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAK4AAACuCAYAAACvDDbuAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAACE4AAAhOAFFljFgAAAAB3RJTUUH5AsGCjIrX+G1HgAAMc5JREFUeNrtfW2stll11rWec78jztjOaIFAES2R0kGpP0q1NFKIiX/EpNE0/GgbRUzUaERj8CM2qUZN+s+ayA9jTOyY2JqItU2jnabBRKCJQKExdqRQPoZgUiylgFPnhXnfc87yx3N/7PW97uc8Z/5wdjLvnGff+2PttddeH9fe974JzXT1tx8F3/s6cD09BvAbAHwfgDcBeBLAKwH6ZjAmAOC1Fh3/x+DjDwpaJ4DHeqr+2qioL9qcf4DZ6cPWBWtaOKJtfkYACwI1bbaPYfwbfaZfr96Wx5Khtl+ioR1nbA3a3LbHRxzUxcKTgndbugTwHED/B8CvAfjvAD4I4H8dLqbnLx8+wBNPfQ6dRFWBy79/D7gkgPhbALwNwNsB/HEALwVwETGGG4w5DpzWXI5IcgXPKcuBALCTN7bD+YJyx5W2r2hbBZf8xRnQxwCIM75Ynox9EM/s5Vxow/Zd3sTKhHW5hC/zQr5ixm8R6KMA/iPATz94ePnFe4/cw+P/5jPIUiq4l3/3Hpj5UQL+LEB/DUeBfaQirr3i2RmsnollCsRDh1nuyo/pO2rnjA0zg3n7JSaYR/5TWHerF42v0mYJfY7Aj1lHoY/HppWKaY7zugs/CGjVXfi+ds2i/kMAHwPjXzDwcwA9/8RTn0aUXMG9fvdL8PDe13FxOb0OwI8AeDtAj1pKvNWm3QOvGykUbpnKxInBB+UqbVm5B4acbn0tuLW29OvdZGxevSEvFdrCivDpWnoUWLd9xn0AP8PAP2bmTx0OBzz+E58yJQ864+G77+Hzv/m7cHF1760AfgrAO4zQcs747Qe5TGfWQqsH7ra3/mZTJJ5YXpvcIXQsXGeYcXBVfxufL7RBPVPE4R/PKiFqamVO0q/yh3cJLU4WWh7+zdKjAH6YQP/+QIe3fO3qs/i/7/xDObcevvseplc9xNVvTH8GwHsAvCbwrVZt6mqFhnsQ+rWBP0uGcRGTC22UBhP+GFg2FoxtyNvhl471cgsSj01o58z9KFyLHn39eMDwhPbUJwB4FuB3PXj4xH955N5X8PhTn9GtAtd/7xFcXV2BDoe3gPkpuELrRbCKURvjrF/KurqOkGPGWeG5JfTANNkR+oa/fqzPGwygxla5TdR1D0R96YOnwVgieJVrsPEujkUqhREHqM8CeCdA7weusfi9BwDgf/h7cX19DSJ6HZj/OSJNuxKwMI22ToX9dkaZCa1h/NhSEIm7dQM4RzLXZ5zS6IxNMKijZUv6CFZo81YEbdypYVwLR2hJWC8x4LStFu+M0DIKt7AOMl8D4J8B/DqA8KW/dPRaDwBwdf93MPuxPwrguxANjG22pN3zaQEwqepRIKEZQLY0h0KhmtR+aeEeJBozNZHLwMil1h+vQ5Pv09rYdllQCj3gCgEYf7es1/CcM6ENhR7qmWe98wU5yMsbAfwIMz86Xb3qmHP5d+6BjjbshwD61zg6x+GATZRc4LRLMLb5tPuiX+m7BQzIBr/DRMULq3YPDFyW1t34EtHtj42GOUAudAZjJd1ywhu/XznOhC9oYNAdf32se0Qb/gqAnwSAwzGIppcD9C6MQuswMxZaHz3Qfq0bAUfC7g7AEQrWzW51S6E1OUEfBU8WpvfrUj42l1lwhDZK2vWxtoCtlRoQlK2e1u6x0A7tzchH6Eu7gZgWWtEeADwK0F8F8FKAcJixqbcB/MZogkafj4Iy7uAzjbIEHM7kRK3CzxN+VRkgjfUHjWlgoQry41G+HKFraKPctSB2FQOPdRH2Mf6WCE7ATxCtfVaavJzXxXGiRn3ysxfYT1ra7wHoT4OBAxE9BtAPAHRPMImle+D6R5awY2gpcEx3cKxWncBLBb0eEyv0oMRZq7rRxFHIePkz0UZzH2PwJ4kht+ERgt3qkioq6XPdMvYESvZpsd1kLJr8aFwO3/XYVPseOvEIgLeD8NjEjDcA+O6tFRaa1dUK6eaB61zrug7ntp5S/43NfC6jZGahXJzKFnUQ5wcaGGoNeSVaHoX74ga/kWuWj81UCefMBHCCd4QhRKmgSlb/H/Nd5avm3LXUJu+PAfjOCcCbAbxMF+W4YsCohkbmG9T1J4YCuMwXWuMNnyoUgQl08u25iKXwPl864YGif+u3os3rbwGaBxm00Ib6LXxnV1/Ewd5Gei20M9dexsCbJwDfi/WUVzq4gdtGIBe3LYmuo/ajnSndu55sI7Dkn76a+zXoQVdr2ejfRr5x3aWsCWzF2GhsdOblEKw0cdCyn4TmtIvEiuToRqT0NB93zAVwQeDvmQA8WZr3NSPsYP3HmN/OwE7Km58wmGcNGwqTo2nTiVL1FxW0MLm3sSFNaNgfGxpJCDwD7OyaRXzhvTxlGXTbICl0m3hj5L4+pUeRaFqRv/GEQU9OYLyy7LSLtc55HW1kEYosmHLIWZhO5BExP/ecq52LSgSZHZ+2D79l5ldMbgbwYwjkDW26j3he3TnLFwtxZ/WnLlgyNlFERloEfOsE0DfLajuCFdNxL4jjgVlLDOkyoTo70NRE48TuCcRUsO2XyVIaiHWDqUC4ncUsYa/aXeC2MG19S1XQ5Ut3MVeadqX7myZgfN3Gq2j4gtHviwch/LaQuOPcdqPzHeiGItwlo+pDmviNB0WEvndvXqM4vENox3mLNwc0S6jwlfJAjLPxB/Ot8HmHBit74U4a497k0mAbNCqsNPMdn2wRHq7qbYPItxFtfR/O60BWhmYPV7TlMgsUaOnFQWcxSz0UoBq/Q58YUO02qanf6dNaZIz8Bw79gdACmLVtixCGMM2clWv5w1QzGVaj0LJaw0GPZZuIxdgHNxCAcmwRX/x+rSceWKBhJuMD6rH5HnmpnaGw37HpHRj0xkWyspLjtHDlWvF96gLcTBiO9wXl3B0tJ3Bw/dmob91edDRwPwSkf9tXZsKxrZPefSm00khMdb1tJ8AxoQ1tqRQC5XVl3mbp9vDUE0Dze4VS3dYC2ibboCm0+iMpThvkkdYoHiFu393of843zqjyyzKGM9TCL5ECrf7C8Z9izbK6rs9eCS1XizpZVNliueGiXZuNtG085zzZjg2nHJ+o57sJjdKcXD+Yyhjvuy/CbW6cVQ3py3z12XzHC8M+CMdXBLFjXs0bD8rLeafrjnT23xQOshoLclRwjU0NmiRTNPApB+F2ykH7xn0KhDuBvMgELKpHE13nDI1o2yLe7sbE1kwYLLpu084glm1dwQ6ulcBoe7r+vlU2SaGhXwFvejzReYpHgQIYJGwrq4IzkkI7+DWp31euqB7eas1RtiI6JqkwkQLSUyaq4XOiLbTzUf3lcYU81CQ3+EkIi3b84SzmCOZbLpCuFRE/j/txsk93vqeeAEREe5qrP9AQRJ8Zy06ZmL5Tg7NIE+YYar64YlMY8G8eehQongbnlb6923Y2jlwYWzzJ0QMqfX0Jh/nEFZrPqdl5R58Ce9AUvN6xyaKNDM6rNV7oNhU+I8J6dvYXoRM7fh34MA1ivfEde1lC+9TE7xHaBuQ1jtNpyPJqoG9KGp9j3f4WsEEdUthrZFuH8bF2lw59r76bFwYa8mgi75zYlS9NZERj19sf8Rg8we4GgK1g7ySh9ZCXY75ldQtVYYCIVlTBHyyhKbT+nVA1YewyvR8l06AtupsLcR9x35v7EqQCUdn63ulWNGgbaege69Sk0w0WpKGlEe/sFtpt3tZgTPi4edDgE7H8m2sGn3Gr4IU4puLw0CvvmdzVg5RBUkVfT+DjPGsJgrouOoIiU9alcDEiVSQplNcW2pq+vTBqNgyG2oCIg4G6sapTVYVkP2YAMljJfO6GULH4p8e89WyEM8Yc7hna2GFFQv5l7kWF07q3y3jQWm9Bem8Ld11Cb/vdXzQ25vCan9LB14yXTNkc/0UPmF550X0iQDK8FQ7Uhg96fh8Acw3p1kTvRkifeVHdTENJfzblnRjb8acbKLrY+vKYmCoBMHXJLzf01UGP5vyonHBv3UBsvG4D/bk5lvVRhagxzTih0rTgFYMn5NpkqD+yO/C5KaobjiPBMlvXfBrmngYLLWNb/XZeeKMFz0c/+q6FLeP4tM68LQ06GDtX7Uuqt7nz5sxLsT88uUxuDLyOdvWwx2FUg5bReD+YInjbPTSu7gxYd5vZCXk1gsQsws5vNJT8qfkR9733FR/eRl4qpZHH7hrI+q0s1fE5T1mnQw75g+35bsbEZ2mYlE0bsWo3G5Q9RtcW2k5wejKkJAstOsxObKWIim3pxF9PhZbrJjnUyI12OqjKMH8e1jK27bgKpgMBi+WRsh9wSKe8oRWgzUzDqpi+9mmlUvBab7pG7dvJ7gut9IcbxKd998/xSquXIg+Khl3vEyrejchBUpc6xxrXXF6KNT8Swkr0OJ581oGcu2NE+YpNB5tMTmf8/ux1DuXkfHFpcfPcAGd4HJvvLfiLBpaPa5uDvsU9RYGweDJAlw7fp6qxHiGxCV0ibM4HQHLFBuUi6Kj0t26iLQHvyiYhQDs2F3bdVrk4lhwJbRSXyPwQQ434XObFUF4vWByUDW+SIpTjyAOn7nAe14M8dGjkNBS4DKvK3+kPdfBRM3iH0m5dGupHEaxDolLzta+/+LSp8Om8obBLX4Z9A7teukx9S4+AQGjVgvabHJ5pVCXh+9rUcQNCMn7dHCAjfD3za8H6IDBTg+KhrG2sF+xEAw1TFv0Gq31REAVOOwS0FvLa+q98WuspBvGFENo8PqgRII+WPCmLsPGGvHJzEfJlg8oFCbbHGme7XUBjw1qRrfsRYjBQNbE6iEsYzaTfHbTlAmY7whBuXui8FipybFGAsKd8LspTCEnAIyZ22dwJzy6wbtbRlk7gJECx9RHp0tHRxG0IBidP+WkYRYD1cduaK/NvGtiubOoEfyv03ZAKhdQKmwYV5Rx/UY6tNz6XvlaQGFnXnE9S+FpWRLoWBQ90sDgK3yqxrm+4KSqwo+TKfgf65ufmWKP0UfY79DTSFpVT6EEXy+R5YGY4RpjGFmkdsdXOOmolWxccbP2OdUeKttj4GJhyo67t0zTnlTOraeRlzJdFsC23imA3c602RTLMPUVlTnNJBrlYX90xZpq6GlOJUbid5+N9emBpP0rOaHoJcDEFuLFleLTKrYviGOXsWw4u7ST/Va/zuaacxzo+77LAjoqgBpBRPK6vwA9fSOYghqySFArtXkRKdzgu3UXjqmsT9qMHpXvASTMR0eNvVjTSARd/6q/j4vV/Eri+Slts99lzunakOqg8uY2gWU/3e20RHXD5uWfwwk//OPjBCwhx4E5glgmeypNaPkAsliKsTxFuaZoxScrpK3zGUmgrl6PWFPIHASAcXvkdOLz2TRVn71KQmK+BwwTghVLTDnaHpLLqdOR9tSfGgpc/Mmh0EtHgCTtitaatzGNU0TNTfe1zlxppp4Vh7etUUJuDLDWFdrawFChUWk+HUaZV87xMmHTE2twiHeuwzr1Lt5fiQCyB4qK6zNkdb5TUhxODqzJTqvFMcEvh86huWKQBmY2Hze+07S0mDvSWB5U1tPQCDlvX1V8Yoi4vjqDenJBaOrit0V9REiqLyLa7NwaEL4Q2vXt3qdtGPe5SmsIg/IQXNof67hFYsVsa7w3Ux2cJwXlcZojvKiggKf0Cufwz3DkKGMCZlh8F/uwIwDd6quaISp5vCse5qsB0Yv0F/xWftV+x9RudDhNCK/7eEUyNxOQ7Ttos5OXu0jlTB1/va1ovrsnqjruYNaFmA6IYlBqYW451zk5Tk8Ex0cDutO6Zk9kBLcvFAjvnFRivZ5GFkgz2s6YSQy335v3dFdIZEbzFGvt2/BuXi4Q7DXzzFPE9trBBOa9MZ2Oi8R0Oj+jgJhu98jKfVhz6gTH72Vbf2ISnZcNlfyew50x7b7AZKmLX+4S24zWYchcQAd5dcURIXk8vj571Btbf/vOK3Pm5L0rycNVobnmskriDiWwMdU0sJRWWd2bk+EjguNbXSNyDIdLrH9+TBI7nqHonxDbk4Q4NO1Ny4bAEjpqFirrtBW3lb8Z47ohsY9oednfEvPMDUYQZ1FWBWHU6S5dbzkfcxWbnSI1zJEviolxxyivavrVtB8k71pgTMFYOxiEyY58pwPf6jBw2J+6U7k3TuEWk8dLx+ZZ3qj+cHZjJ6kV5E3eFlhuENAIxdzcsrTs/Ni7NGcT2+grg65u3c440mhOdT065KlFR9uICuLr02rVWlAPLuoumBqKQ3Uau8p17FXLCQtiq2gLmTNN2NyVk2Zu6Cpcf/Clc/ur7ALpo0AP4t6En5VUd82g+sL+4PhEP/Mr2uZRx/7DS9vgAfu7L80Hy0j1guCe1egF66IaKxWXPxsR8Sb8smTHvjJrWY6pugfeYmn66+t8fx+WvPA06TL4gbpliu3Ecb32IRJZtXaIcRNUCYhTaSfTrngdwNwWInEU7jndFAChUIIFm30iTAknQ97jFi4+Tc9zJBgSsmQonh5yaJGY2FLwAPbCHeW7Boz0cQIcL4HDh45HmxkSPB0UQKyZtmWWqv7dwrCuEcKvtwFFCMrX26vJ97VsIloHUG9v+R23pbjqVwG/5YgKqY41DBwuj7AJzGU8LAal7EJiZVSFEk8trF2dIm78cCK071oW5QoMEvJOkqy+lR2SJgfvxBbPXiNSE5uBKyDutkCJIqrbA6WcDUmQiyneCs7Bipv6BfMUO0EccxNksvapT9+BMWJgfE9UTu2g7jmdYjc3i112Xa6TR7GRqi6QEY+Fp+/yAE8+IPivBczeuakHc5r3y648p/FzUJkjeu7Gx0K4LOjLxQRAiNELRx1mTcGW28Yd9pmas4AsKCxRpT0FqT4BOWZDjhpBsq9lnZeIrZVNesbr1HV56pxGYTbBywohQ31kV5bGjUXTZlqlpJk9AS03epW0ru9w0IF4ND8av8xtumVNfa1p3XHL/seJ9KXQFfZlCGrD5egv5+FteCGLgCO3TeUmuWPIIdH0x8cTRfKqPW9smWxjTCbKCuMINYhX5KZqAcnzWIg20qXxDjm/lHEL0JSFN96C1mCOrQKuFLpAdjPyfbAeD898izmFuK4o9Rq8tedyLcXbTOkt9xtvtacs/UX7VJt0DKf7i4Gjx8tiM49Z1DoEHfWY46kgXcaIp3f5UG9GOncP7paVJE21NGXJtEK0UUdePX2j44ZupPav7hNS1IthxpxrUW7EE+J88cqEip1RyU84Si2wIx8DLBv8CLd1VXJsVCfoI+BsEYi6C4tel7X5cV3DSQElhdm45n4g1Ih/a70ysKXdj98EYVPJW+x6hHVsrN00CBMCMN/HFxwB6hBGzG8pl9zY4qwRveeaf6pvJKQO1nqxE1mA4HVbTajoooZ2EUS1nX/Z7a27uOoAdzCyQh8bmQjG2xI1wrNRYrGW52lirU44TtKkQ2s7mwkhf5PdOsVZtmZn5Sp4OHpncDngC487mMYQ4KHklg9XTdWliYSQR2+ZB0RbMdPtQAxlci1rLKo3s1h1GYMe+zlZxTerA33pc0+LDSfA/rqg+Xkdm3YVXhOo/wgGYeuTVb2N+RSKvz3G7YHQdc/eFUrooXHCDAg141NDm1YSLdhN0JKqr+oiVD3mN1Is+fT/MHkSaRHtNWzwS3UMeOrBKB/iv+jkhLZPBIodqYbIMiYsM7Svfub1BcNLGzI68arGQp2UdPsocmv+h3thi+dXoRvFJVH9gIbTTMqMljrkpc13tXFpWdauCyyCKseNg5uCKU18QjPWgi2GYfbO/YboMXF/b8luddZNBaMjVRBBwuOjhtNHcBgtq7HgQRgt5qfZEkJkEp+o8bi5kPkwiHqosqoDaYwwq+zFVjsSfX2iFFwD4GiW8wfIaF698LS7+8Jsh5mKHD3z5yQ/j6vOfAOgQ8nAMgOVpMMbFK16D6Q1/wnF3yPwh2wFAB1z/9hfw4H98ALi8NMW3biKjE+O0i6p1XBqreUP0xruVf6s7+QJrG26ZwbHTcrAAXBPimKRbEVrLIAO1JSfQ+PoaF9/2nXjJD/4ocDic1PPXfvKf4upzHwcudP0GFHh9jcMffD1+9w//g+PbDCeky4//Mh4+8yHw5aXfJ2tsV9EXIkdH18J5vmnc3a6fLDN1TFSKRxa+0dISe5/piSL3+Z82dHIrqUY3bgzPWf8avlAkFuHmBNig0YWiFG2OVzXAB1FdZzIDaxEJ9tzJ6OMGqyHoL/R7XJyR6vYAjCft02+AMW73/fQEVVn/uU3XZUujXyyD6AULumFi/3eJK3OsyMpLDkN0w8iUP9EsBddVf63BztXdhesxIPCHZaUMrKWzCA8LaVgyT8NGb546sNV5FwsvwxVdH5dDrt2D+BUFtjwUbH7B3dXQDOdzUYuap6heITAcqfjlN9vmqDIPzu9zyBA1csxjYd5vKkiqnWo3jdOfp42fNwO2BGI5xFkpk6BsBuct8QRV5TaeiC1fqS39igkL1CmhPk7bwIK5tj+npgFZSCAsB/g/L03BhCmorubUri43dcuty+e6cc6eNL/+lPHSaX8+ZEPR86Fyx+9zBrz8FiDzoGWR1Vv7pmbJXanlTwPBwfjbcLI3fq7RfART8nmszrG14pBTgh6sCFekbdPdujWPTL2hI89pnMZJc1d3yJ1I2BXYXL3FuzbS39GJBf/8KX+d/OZpxLqFwC7jDybgVpT9Nmr1UC6oJJiCLzvduCGSO7uwDkt58iqvv/24zW4VO2BzoivJC44Cxh7/i8DwU1O0qBbzuYzz9hbJqCyMUBga41k6L0VzUgpF9MnOTlFuRdnkZZNIi9z5bU4pvLNuGxpVrj7S1ooQoVcOE9CJ4gdYbVsxtzJz0qc9jnOHS3NC8ncjqWnpbkaHFUnfvIssxSvV2nIa163AocDGFj7ii3+TjRRG63+sUWjNEneglQmpvs5y1mQ1G9f0vQh0iSTxzDM5t6sSGHcJqyCxUjQKGTBZTdor3TS5jjnFqrzUtC7G19Va5D70txvPJDiOOYy12Yuw6eBrIxFQrPSdRYDrceZCp9xDjuqX/vAsq9H6lG1Opi02MrNqvhyjbRG3q24TpD5bWg6HtOg7F44rCIgx00UprijDbfAhGBMhQTdE3S2v9ua6QJFRogwQTXVjO7RlgAB4A8vqxsjG+TWeRje697+eRW54+C/DSGnByG/RPeF4e/UEiznwqLG55CI3od9LgLhmNN8N24ZURo7WN01nuRDajFGLCbg+8Y7b+fKtFPLKFuPZUh/8jwGym/RNwHb/kIIBGoulinVKoU14GtSd7ENZaHWu14MdcaS3VOK2tixci4wpzHj43/4dLp/5ALbLmeO63iRcP/s/gcNht0+7oBx8dQV+8LXjsca90nQ4zBcrd/o9e1wWpcC5VUXU+1tj1TbGu+X5lwYWdSf4V0GuRfWB4Ay2iffWe2aFkgnzBnb16x8Bf/LDCoVYmiOXGZLXF1gPcQd9aFrWrMMBV5/8CO6/52/ktAbBCoFw9YXPOmd5Ny2VL6gI49yXvPibQxW4GFPfpO8S2i3QpLRckOff1jiLQXoe1gkkfNijZ35HO8VBGTueA4hWXHkT3uCAOob2j1YkhByLLWwARLj+6hdx/eXfdPsJA9mRL3RA69WdMu/0tFrTpdlw3nKsdcO+e66Fvyh7rgVDXXq3vt3qz+YgGD5x6dah/j30YffmvbrLatpo1Y2vGxWBGRtpTDHodG9+aI9o2Xs0def1FFih2rL45N0CmqAtJhGNW9ARfZ4FJspft9m3BZzzZHKqJlyLOjSfl0S48nj8vwL9wwBQC7yvKbPrMDeoS/Ub9TPk2T161Bo5YmGV5ui+tkDnS5pvIU+40UYxXh8ma/JPfy7K36UrBE+UGb8RkPulegBSlvLBH4VPrg+zYRAwl0z5mLYlb6Ex78NObGgGKwB/VnljF2tbmvCzCfJsp/iQN2kX/RBUtQMxXwEkFnLrTAdnUJelZaasML8lLKLyskDP1Atgk3wXaQh0Gguj5GdzXMZdDHlTjy/i/bkw3YV/qZuntnDnp1QpC13XFqWThBYgdZA8cq5Lnw8r422yvotdcbHvtpmWZKISgV99rvYrOclFHZ2Ay+ujsSD7KQDpX6S0yFmsrHYEmaXViMscXYUMgG+B4165xmoMB7/Vt4FCRZvXbsftUUFii0bkGqWhZdnmyrKNHafzpkZkD6B1VoQjNxQtgc++/TbtvfZeRJEZ1uhuke7Aaf0+paUqNk7i5Ne1B46ixUdCyrk1Dvt7wXOlP4tAC3eRh5ukOsbolDsWphle14vZi/H3xwNTR42PWmGErWKh3XyXLcDRAtXwS+sR+o92atpjlsR592CKfvuB7zwg7qSrvAi4rW2b5vnKfVpRp9xcILMoRTnJU+GZuZtyTr+tu8O0j1muPDO/qgXfzIjdLitIkjFZCgOrhibzhdY30fvcl3k8K2GBZUghp4VLowq5QRLOqu5f3UKp8tzxKr60kKZx76IKUIeegu+c2YrWd0s0NfllcjzS/+JgjTx4g+3eIt4N+IRfAKSLKumT92pWb8GQL3CnJKvyQz75c2eElhG9RFpq6YpA+UAKbnJuIStjBJS93EbddGLDMRSDj9oL4Bm3nZweWvhUCJPFnmv6trrRpsm5Exk6yw9kD6q1di2a+G7EvCU42x6sGnsFlesI2W+cvPoVVBbirBEDPOb14ajAHCkkzDODEqddGS4bNJpnxMd3R9i3JanGFfIsZC9oV8xjZB+uFp1INmGQvzlnzjMbEKLBefail797guF+gyCbHNpAZlcbRXWvr4931ApOI8CF9UJ0R7ihFbSV0QLqaz9SP3aeUxj8Q+rUvbo6/RyyOw+BW9KuL+auA+2IOZBGWvDEJcL9eroRnqY2s/3UsEYsCNXACRdPvgmHl38b9Hlct1rrHbUYCajK5HzSwyE9U41ARo2DrzG99o/i3PeSbG5CIxBTgpLPXTfAzdGcRcYmXWDwb4ZzmbWW6Puluq71hKFXnq7PAA6Ee2/9Qdx70/cPgvuNlujGgjvGeSPq03LzjPDlMcK4C3pi8LxiKlNUaLvLqY+FyucFQ90jcOOo4kHw0Mbx/6ddrHyX5jR65M0dMZvqAHP5/lp49LEMbmmVeOHj5js3EYHeAfIaPciZkgjt7YbU36Bpnv82etB9R2zInnUlVy6Ek7dq6kHDi50zAnqXfHQGoOu26ozkSXRD0kZNn/Uu1WlRWh1lkyizRCDX3bAaB/aT80nZya/XiXxjJnRYleyGpXAUtRfLXWqlHcB/L5Cy9Vec2zbnfPi4R8sk+s1WlQrEOCnjdjpDVL16lo79GxN3aVdad74cJkdy0TH77nkOBdon7mXkvky50ObQBDp1h/r9vf2t36Xp9cPVuEu3mMjMGwlXcrb6PQUS7wdU9WrXIvmWL8QqjJ3xXL0fR3oaMrENoP9ttbt0QvJ2LllOvNnFbymzxL0QD+2xAgcmHuvw1PAr4vfJbiS0dd2YKTfHL+/SmAj9eKJ39gDq9VenrtnK1e5gsilD8b0KQWPbg14U2tSUAvEYNyGTIO4unSVFgVhnzp3dsBXyGjVLsr0uMrqvtzfO49oD4GgLbcAR9cyerlk0tbvaXWm+S6el7vmEZl77XgVb36/nTfSxnvu5qK0fGp/MmXkwtfO8gDAnPLRRQi93wnsLqZrbIHFdJBRaRv0GtuM2qvO44/8cX6PyS5Phx+ZCHwvcw7w7t+HGaVE23H81HlE527YDLEjFRsFJPkmfzGOsFwhZh9vqxphgBuZ7uAJtG+JYtESuJmgVfbj174T2HKnYxSw1LcuvNrGoT6QwTOsjn3pl07S82Kjrn3L2IA7EHIKxqdsYG3SPRdoPWt+lGyT7xkNXq7JVVPJgVnE0sefu+cpwuR+XCc4XZqqNBSC/0bEj8OFlIDJ/YOj6ctrVp3/leE1ncqA6hvI6TMvGVrWzb21Z3JKKsyM37JMOuPz8Z44H0oVw9NyD9GxDFoSFPDOK0GwGj/TR//vLv59DQkr4w9uWRa2lF4c8K6cG5zNquKazPCrnoSMFncM4e8FmBg9ltAV0sNGAjOCl0nF8K2s77/gJ4ppCm30adyUggNmau6zVYlmPNVrwQQT7ElgAzOVzISFD3rLaKK4YDtinj4FrDuqPSLeqLfrwQsptr5lVW3ldVucxCMM1QQPg7vcZv2kxVzVvZA5v3w7XE7DUILIt3s62GA5XcQQX870iPsn8o4NoFpsQDJ7cK3Jkx6Qrjvu/icC75HBWLjchsztjGGV2YMZ6ngnOGJUfvxzlztMIqyHz6lFUT/JnH44qlFvvpNd6AqHNkyGvruen0UXI3bdtvtmRvcVdnESlUMWfvCMm6uZ3lHmMECsvcvxdobW6pqFRQgy6EWFvvFEWavzT9+cXC9bb1w/orHYUt6/qKIOXugebenY9FGdXN5qLhjbvuW3H/02iTnl+IHv/vaqblHOI7n62KZrchc5E1gbbiiRRNKmqtbWpQBXHQhsqAmdc4vscaCoBtQUrXZmknouze23HvDlVaA3SNCweQHx1J3eY8w2rmAH55kJhAptlxzG4roFvCaQyXkNU8ttPUhqsCPqCnz0TD/s2NO2oW5fxlUrCg0xoOYItc6ENfFoz1qk78ErT5IRl9eWfvkanuC1OmwySw5SIcWXkXy2oRIjcuyTyic0DG6uhFkext5hlv/3Loz3+YbfQxlVtmanUKumKo5CTp3w+lbJBeIyuAqkWXrinrh5jN9DzSjjXiwZ13Wi8c8NMFQwl+VwJT+r69ayjba7fln86bN5jZmf5GMZ1BDRgqC7TC4oGrDBqLkx5ENfSZEs57izGLVATweIJmx8bfb0AJr/tMuNJ5voUfJkFMnXzBFRHbUujK/jf8iUQR5sLQL2iduehqfW03x0cU8+0ES8ckJp2jxlNEQAJlxk59YUvX4VCaBvBaXpFVGVgK/dFWTkZypGD9aoOqeZvfD3WNjZ5k8YShnMkEAgYJ9GD+M0HS7Rvkhz4zvFzdwutaobMg5U+9sa2PKlhK3KfEjfoW4EJHdCfpgV9+oKxwfvwysAw5R5I5MYVSIueue6LCoijoQ6Lyvi4oUNegOd6Qs2qcwBA55BGM0r2XIsGTjuYMss8t+Jm6rfFGOO0YaC38KelaUctTXRUI1RdpMFqbHv5WV4G4vNp89d990NNFCUtIY6nHOsqNyB2p9MiQszLyjVHxkHqCC1gX3HKTagp5wcbQ2c0quGgjy7y4tPn+5sNVcub67M/EGsIbYbccPEmrxqb2hgaJq1WmGMwaz6J6lcOGb86zCl/q12fouxp6IEtVwcqETmVY2jLxNF/2LcJ5FrCV1nCqF46/nQxD3UDyxe0PY5LKoF8nKxyVsENjyYmGxOzrY/HPgws51OCTggl2voizlyjAznUrgWH9c3ECkrZ/BH3MeQTl+V810dqMQr63xk8V0Lb8qWbFplFrVWYPfdoyZqi7+JWK3muk6yYGBskZBdLSB+R1ORsfbMX6JDXXs8L8nxGiz4EC1kILSGyQDQWEO2J7PaNidLnrmOF7ZmPyMzNBOZzmY/+QSDJk5AfXiOcNAZ9BZNlitfRkX6KyngEGU3WInDUIONc+99bMFqWI0EuNa3jV4lyhTZq0rf8RSyyKQBffSsieJlZL6ctN7glp73RJdz5jpgZm2SUn7hWNpNfwB/sYodjE4pQW0jhc828yYtRioLeWeVxVS5gpis3DaHtuxZrPrnCk9FYmeDdmwu++6ELcTZnSXtmzqtxNhds6ztnq+4TkFcHPTArO9aCTp4RoIYJjWErlEwvv7JZCm3fhJ7yNaPR+sjhnCq0Td64b+uqPjIoUJaLrxSrNi+GPAWHeW5n1/zUvJilliXE45sZBZscmZ5BZbauEtp8oR2H38CCozG69TzzcQKEOAiFBphLt2Qpy8VWrJM3uC7U1YT52Gb/0muLg2aEqd7amtyCutNU024d6zxPM7hCq3SjcRGqcxGDUJg+GVrJu7FCuo3rCN24sMpjjWpREUax7sB5nibrfUdh4V8PspJCRuDsfbkCg03RA5E/fibW1A9YO0UNtiPH4v2i7sDCa5fqk/2CXqlVXIE3UuyD6F0s08uMg5UxK/Wlxdgq2nz68jew4/qrkst5T2E9r6gbz/gLMuPdklIfl9bBe5VjgaegvYh5MvKsUgqz1ZZB9W398FwoVo05093Zxo2cOmvm/brj0x6PEJzUyi3DtqC6vno8F6JMIbTZ+GXbGw+mkBO7fLIof5+JryPsnFF7gpX0CtSkzxijVf16xmmsX/qMQQDY0bauJjMNBYEyHNOdIw/bcDsBZ8RTtMous+YfawxNVMzQxcy3iQiYRyFWKOolgyZHcHwm135fLwj1eOcFVAvwn/KzCnR8gXVHnJvfHCXYAz/2gj5/jHtchAUrOmrcoYAcfRKEEYy2zBkcEqbA9wgLVfFcZ/cmEj7ONCZKoS3fesC2kE2T7YVAkdKOK0D6tL4q9WmOg8Rc0+blghT5tY2+R5dLfnWnq2nZtFnQLOsujNJASSsgdH23bWcnrdsx8UG5BVLysu3Au0KbT05nYlcCFF/aboUl1H9Gti677WXta6ssEIohw7YpFRwN53GdYCAf2M39XCHwHQCeDb4784EonZwZElu/t5WOTQrEqD2daFk9oKDZtgUy/ZS3dCMO/jJ+jrQRa7e2hx4ZXobnaROBN9LvoC/OopoAXIIxpUFHAN7XsEdnhUZ5eRnuV5y1ZeWLBeEIY7jnzCl7U6Gdf0eQV5ZPQwGLzHTgPFosoC+0wYLSC/r4sJYd1/oot1NSd1y0zpxdHgA8d2w0GlhsCuRfEewBP18xz/axMZ7nNvyF0lssIRkZ1rk8iwPFRHkPYwj4NxLlOiGGaIuMpH0kmtzPQNiWdAcJYrFxxr+YfkWjJc+Mfa3/3IEZv7H3vqqRp2SYpDpZH6uJZDhCZ/sdEQv5wFtQjosQuiCUCy2ixewRa8/ECkZ59A35yv1xaLJjI+6V8zjLoULab/lyTZto25U3azmphvM3K75wAOgTLnFqtbsKAN69UORU2Lcw9MAic5lpDO4gDxFyACzXTTngm9Vmo5lmh3chjWpij7zMLchSrwerWeHpnG3YFnzVR65pBzZIMczmzX1s6Pu1A4APA7jKGHXUeYGab0SstbZUddXV+sb7TBi/CV3CVEcTr9nDMy4HFPRBQYaD8Rr/rYRAywJaWQhjTJXQulrSKZng7Azz6v92FLYKonn1u/2YiHEF4EMHAn4JwG/FBEUaj+zvYWLW1Q2ludgbvhRanmdQQyB9rBEZzVxKR3O8ngVK3YN1/N7mRwO6YjC5k0/HxZ5gkQwCU+MMiWMhfZ82Qh4CFyT2V49VmRqLkoCjrP7SAUS/CtAvr4GOE+XJoLow0yxXysznAJ9T7c0Vwy+kN7QFZYzjufmhORY/EvPo+EpyCVKtTVCc7E+w4HliKa0f1W3MWRQPsF6w6U3jqBfGrjw3JvgoM545gPE8mN8LxoNocLnQFnDH8ssPxByKOyvWlouRh5hWGv72t+0bWnChL1MWHP1sa1pd2qOPwhKu41j4rzov89tn14yzulkfFca9WYFLAD9NRM8fZkjlaRx9XctQn0lu3qKfdc3+5XBd2CbUZrLkslgCwdmHta59SH5WsFU4aVEZO7ZxA6TCacfnDApe1fEie5lVBn9LeQ5cpnL8s1xwVVfw5KMAfh4ADnOlLzHwrxi4bwfgMckhJAs4TIphoRwHdXwj2d8wAznjN01U+F8O43P8sjM2OGX8Z8ZUG9dH9rm4dey2nfuQIX3JKa8URKuEFqXAj4/vM/AeBr5IBByeeOrTy9OfAei9pR82dqCYZyanEgrIumpkoXlaJmVDAJzJJfU7G0fEdAfyEmPL2ks1rWcJrCJwr6hqjaFvQTxNLV/0TXiX+bWVe9Dj3VjwPwH4WQD41v/8i8dL776ObwJA9wH8GAMfy5kcd+r6X1FafdJIKGKNuQWLlfn1gg2s4L2ll1RTW6Qr+mRnoTi02LFFlksHQImpToSdh/pwSgu+WKwVRojduMT0MXCogNY4a1NTLNr6GDP+CQH3n3v+nnz61Xd8+/LrrQB+AqDXpIxyfKN1chvf/PUu+ognCCzQgNY7Xr7QRs90v8fxlJp2HrbPF8GTpY9OkGQ02UlCa8txUVeMKhfaekE6/cS8dy4hWX9+lhnvPBwOH7i6usSrn34fgOGa0Sf+7afAIDz+4PB+AO8C8GwdcMgVqwjjsG6FZdp8oQp9sY2F9hSIZuxlG5ERMvL4IngyttLBMmMPKaT3JkJr8yIUB0i36Dlvk5FAlcbUrQv/WQb+5u/7PS/9wNX11Sq07ii+8hdfh/uPXuKx56e3APhxAG/0iNv9JUqhRU6AnsLgD4rRQfQ7lzEaUNG3LKiBmuAyDB+yCgPUQlv6Vxu5ykJaH8WbsC7ZcqG75QqtHZuwSJlPvwVa5PLeH8PHGHj3l164fv+3PHLAq3/+F6PRbekr73gSwBUIh28H8I8A/DkAjw7E2deSuCeM7st/RV0ZOPRMqDTTidlTTOfhSYyoKNaFyEND4LEtqj3tj30QgPSbG87RwsUvzXmJVaBqly4P4lKrJ4X2PoD3MuPHDtPFr189eIhX/8L7vFbj9NW/8B0A+DEQfT8Yf4tB30XAPUFv6LcMmqGCyhqarL7bNvBpgfaiCieoEDyfxtpXX2jr+fkxpJSMzfjgvsaLFufGu+a8MVRn8sulqZV6AOAjAP4lM/8sge6/SmnZYhZk+vKffz2mi4e4up5eTsDbGPgBAN8N4GUAXaSabCX+mFdrS8uA3uZF4NNWgZhiXh1MxYuqDsJk/djS5P2uNLbeGJH5oZuWfIjF8qTBl4WfVSDKuALoSzgK7H8A8PQ1X//2vekeXvFzTyNLpeAu6Ss/9CToEYAv8RgIfwRE3wfG9wJ4EsArADwOfTU/R8zqRNdorvbAp638PqeN3L2ING34HlXabw/Os2UCgZeaPPE3e3i0zA/NfHjB4XZ7m1JUDwH8DhhfANEnAP4QGB8E8AxNh+cvn7/EH3jff0Un/X9D3uNHk45pqgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0xMS0wNlQxMDo1MDo1NSswMTowMKO0v5oAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMTEtMDZUMTA6NTA6NDMrMDE6MDB9kzKCAAAAAElFTkSuQmCC", is_valid: true, label: "Subflow", environment: "onprem", description: "Run a Subflow trigger", long_description: "Execute another workflow from this workflow", id: "", }, { name: "User Input", type: "TRIGGER", status: "running", large_image: "/images/workflows/UserInput2.svg", description: "Wait for user input trigger", trigger_type: "USERINPUT", is_valid: true, errors: null, label: "User input", environment: "cloud", long_description: "Take user input to continue execution", id: "", }, ]; // Adds specific text to items // https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react function useWindowSize() { const [size, setSize] = useState([0, 0]); useLayoutEffect(() => { function updateSize() { setSize([window.innerWidth, window.innerHeight]); } window.addEventListener("resize", updateSize); updateSize(); return () => window.removeEventListener("resize", updateSize); }, []); return size; } export function sortByKey(array, key) { if (array === undefined) { return []; } if (key.startsWith("-") && key.length > 2) { key = key.slice(1, key.length); return array .sort(function (a, b) { var x = a[key]; var y = b[key]; return x < y ? -1 : x > y ? 1 : 0; }) .reverse(); } if (array === undefined || array === null) { return []; } return array.sort(function (a, b) { var x = a[key]; var y = b[key]; return x < y ? -1 : x > y ? 1 : 0; }); } // look for keys and set values with shuffle dotnotation // used primarily for AI autocompletions export function SetJsonDotnotation(jsonInput, inputKey) { if (jsonInput === undefined || jsonInput === null) { return jsonInput; } // Check for array if (Array.isArray(jsonInput)) { for (var i = 0; i < jsonInput.length; i++) { jsonInput[i] = SetJsonDotnotation(jsonInput[i], inputKey+".#"); } return jsonInput; // Check for dict } else if (typeof jsonInput === "object") { // Loop keys and values for (var key in jsonInput) { if (!jsonInput.hasOwnProperty(key)) { continue } const value = jsonInput[key]; // Check if array if (Array.isArray(value)) { for (var i = 0; i < value.length; i++) { jsonInput[key][i] = SetJsonDotnotation(jsonInput[key][i], inputKey+"."+key+".#"); } } else if (typeof value === "object") { jsonInput[key] = SetJsonDotnotation(jsonInput[key], inputKey+"."+key); } else { jsonInput[key] = inputKey+"."+key } } } else { //jsonInput = inputKey console.log("SetJsonDotnotation: jsonInput is not an object or array, but key ", jsonInput, typeof jsonInput); } return jsonInput; } //export const green = "#86c142"; export const green = "#02CB70" export const yellow = "#FECC00"; //export const red = "#ff3632"; export const red = "#F53434"; export const grey = "#b0b0b0"; export function removeParam(key, sourceURL) { if (sourceURL === undefined) { return; } var rtn = sourceURL.split("?")[0], param, params_arr = [], queryString = sourceURL.indexOf("?") !== -1 ? sourceURL.split("?")[1] : ""; if (queryString !== "") { params_arr = queryString.split("&"); for (let i = params_arr.length - 1; i >= 0; i -= 1) { param = params_arr[i].split("=")[0]; if (param === key) { params_arr.splice(i, 1); } } rtn = rtn + "?" + params_arr.join("&"); } if (rtn === "?") { return ""; } return rtn; } const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important", }, root: { "& .MuiAutocomplete-listbox": { border: "2px solid #f85a3e", color: "white", fontSize: 18, "& li:nth-child(even)": { backgroundColor: "#CCC", }, "& li:nth-child(odd)": { backgroundColor: "#FFF", }, }, }, inputRoot: { color: "white", "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: "#f86a3e", }, }, }); const splitter = "|~|"; const svgSize = 24; //const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/" const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") const AngularWorkflow = (defaultprops) => { const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; const referenceUrl = globalUrl + "/api/v1/hooks/"; //const alert = useAlert() let navigate = useNavigate(); const params = useParams(); var props = JSON.parse(JSON.stringify(defaultprops)) props.match = {} props.match.params = params var to_be_copied = ""; const [firstrequest, setFirstrequest] = React.useState(true); const [cystyle] = useState(cytoscapestyle); const [cy, setCy] = React.useState(); const [toolsApp, setToolsApp] = React.useState({}); const [currentView, setCurrentView] = React.useState(0); const [triggerAuthentication, setTriggerAuthentication] = React.useState({}); const [workflows, setWorkflows] = React.useState([]); const [parentWorkflows, setParentWorkflows] = React.useState([]); const [showEnvironment, setShowEnvironment] = React.useState(false); const [editWorkflowDetails, setEditWorkflowDetails] = React.useState(false); const [workflow, setWorkflow] = React.useState({}); const [originalWorkflow, setOriginalWorkflow] = React.useState({}); const [userSettings, setUserSettings] = React.useState({}); const [subworkflow, setSubworkflow] = React.useState({}); const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 325) const [creatorProfile, setCreatorProfile] = React.useState({}); const [usecases, setUsecases] = React.useState([]); const [files, setFiles] = React.useState({ "namespaces": [ "default", ] }); const [appGroup, setAppGroup] = React.useState([]); const [triggerGroup, setTriggerGroup] = React.useState([]); const [executionText, setExecutionText] = React.useState(""); const [executionRequestStarted, setExecutionRequestStarted] =React.useState(false); const [scrollConfig, setScrollConfig] = React.useState({ top: 0, left: 0, selected: "", }); const [history, setHistory] = React.useState([]); const [historyIndex, setHistoryIndex] = React.useState(history.length); const [variableInfo, setVariableInfo] = React.useState({}) const [selectedVersion, setSelectedVersion] = React.useState(null) const [appAuthentication, setAppAuthentication] = React.useState(undefined); const [variablesModalOpen, setVariablesModalOpen] = React.useState(false); const [aiQueryModalOpen, setAiQueryModalOpen] = React.useState(false) const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = React.useState(false); const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false); const [authenticationType, setAuthenticationType] = React.useState(""); const [workflowDone, setWorkflowDone] = React.useState(false); const [localFirstrequest, setLocalFirstrequest] = React.useState(true); const [requiresAuthentication, setRequiresAuthentication] = React.useState(false); const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false); const [showSkippedActions, setShowSkippedActions] = React.useState(false); const [lastExecution, setLastExecution] = React.useState(""); const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false); const [autoCompleting, setAutocompleting] = React.useState(false); const [authgroupModalOpen, setAuthgroupModalOpen] = React.useState(false); const [authGroups, setAuthGroups] = React.useState([]) const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname; // 0 = normal, 1 = just done, 2 = normal const [savingState, setSavingState] = React.useState(0); const [selectedResult, setSelectedResult] = React.useState({}); const [codeModalOpen, setCodeModalOpen] = React.useState(false); const [variableAnchorEl, setVariableAnchorEl] = React.useState(null); const [sourceValue, setSourceValue] = React.useState({}); const [destinationValue, setDestinationValue] = React.useState({}); const [conditionValue, setConditionValue] = React.useState({}); const [dragging, setDragging] = React.useState(false); const [showWorkflowRevisions, setShowWorkflowRevisions] = React.useState(false); const [dragPosition, setDragPosition] = React.useState({ x: 0, y: 0, }); // Trigger stuff const [selectedComment, setSelectedComment] = React.useState({}); const [selectedTrigger, setSelectedTrigger] = React.useState({}); const [selectedTriggerIndex, setSelectedTriggerIndex] = React.useState({}); const [selectedEdge, setSelectedEdge] = React.useState({}); const [selectedEdgeIndex, setSelectedEdgeIndex] = React.useState({}); const [activeDialog, setActiveDialog] = React.useState(""); const [visited, setVisited] = React.useState([]); const [allRevisions, setAllRevisions] = useState([]) const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); const [prioritizedApps, setPrioritizedApps] = React.useState([]) const [environments, setEnvironments] = React.useState([]); const [established, setEstablished] = React.useState(false); const [setupSent, setSetupSent] = React.useState(false); const [graphSetup, setGraphSetup] = React.useState(false); const [selectedApp, setSelectedApp] = React.useState({}); const [selectedAction, setSelectedAction] = React.useState({}); const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); const [selectedMeta, setSelectedMeta] = React.useState(undefined); // Disabled streaming for now const [streamDisabled, setStreamDisabled] = React.useState(true) const [executionRequest, setExecutionRequest] = React.useState({}); const [executionRunning, setExecutionRunning] = React.useState(false); const [executionModalOpen, setExecutionModalOpen] = React.useState(false); const [executionModalView, setExecutionModalView] = React.useState(0); const [executionData, setExecutionData] = React.useState({}); const [appsLoaded, setAppsLoaded] = React.useState(false); const [showVideo, setShowVideo] = React.useState(""); const [editWorkflowModalOpen, setEditWorkflowModalOpen] = React.useState(false); const [userediting, setUserediting] = React.useState(false) const [lastSaved, setLastSaved] = React.useState(true); const [selectionOpen, setSelectionOpen] = React.useState(false); // eslint-disable-next-line no-unused-vars const [_, setUpdate] = useState(""); // Used to force rendring, don't remove const [executionFilter, setExecutionFilter] = React.useState("ALL") const [workflowExecutions, setWorkflowExecutions] = React.useState([]); const [workflowExecutionCount, setWorkflowExecutionCount] = React.useState(0); const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0); const [workflowRecommendations, setWorkflowRecommendations] = React.useState(undefined); const [showErrors, setShowErrors] = React.useState(true); const [highlightedApp, setHighlightedApp] = React.useState("") const [listCache, setListCache] = React.useState([]); const [selectedOption, setSelectedOption] = React.useState(""); const [tenzirConfigModalOpen, setTenzirConfigModalOpen] = React.useState(false); const [rules, setRules] = React.useState([]); const [sigmaFilesNames, setSigmaFileNames] = React.useState("") const [distributedFromParent, setDistributedFromParent] = React.useState("") const [suborgWorkflows, setSuborgWorkflows] = React.useState([]) const [allTriggers, setAllTriggers] = React.useState(undefined) const [suggestionBox, setSuggestionBox] = React.useState({ "position": { "top": 500, "left": 500, }, "open": false, "attachedTo": "", }) useEffect(() => { if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { saveWorkflow(workflow) } }, [editWorkflowModalOpen]) // New for generated stuff const releaseToConnectLabel = "Release to Connect" const integrationApps = [{ "id": "integration", "name": "Singul", "type": "ACTION", "app_version": "1.0.0", "loop_versions": ["1.0.0"], "authentication": { "type": "", }, "description": "Support-use only", "actions": [{ "name": "Cases", "description": "Available actions for case management", "label": "Cases", "parameters": [{ "name": "action", "value": "list_tickets", "options": [ "list_tickets", "get_ticket", "create_ticket", ], "required": true, }, { "name": "fields", "value": "", "required": false, "multiline": true, }, /*{ "name": "options", "value": "deduplicate,enrich", "required": false, "multiselect": true, "options": [ "deduplicate", "enrich", ] }*/ ] },{ "name": "Communication", "description": "Available actions for communication", "label": "Communication", "parameters": [{ "name": "action", "value": "list_messages", "options": [ "list_messages", "send_message", ], "required": true, }, { "name": "fields", "value": "", "required": false, "multiline": true, }] }, { "name": "IAM", "description": "Available actions for IAM", "label": "IAM", "parameters": [{ "name": "action", "value": "get_kms_key", "options": [ "get_kms_key", ], "required": true, }, { "name": "fields", "value": "", "required": false, "multiline": true, }] }, ] }] /* { "name": "Email", "label": "Email", "parameters": [{ "name": "action", "value": "list_email", "options": [ "list_email", "send_mail", ], "required": true, }], }] }] */ // For code editor const [codeEditorModalOpen, setCodeEditorModalOpen] = React.useState(false); const [codedata, setcodedata] = React.useState(""); const [editorData, setEditorData] = React.useState({ "name": "", "value": "", "field_number": -1, "actionlist": [], "field_id": "", "example": "", }) const [loadedApps, setLoadedApps] = React.useState([]) const loadAppConfig = (appId, select) => { if (appId === undefined || appId === null || appId.length === 0) { console.log("No appId to load") return } if (appId === "integration") { return } if (loadedApps.includes(appId)) { return } loadedApps.push(appId) setLoadedApps(loadedApps) const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false` fetch(appUrl, { headers: { Accept: "application/json", }, credentials: "include", }) .then((response) => { return response.json() }) .then((responseJson) => { if (responseJson.success === true && responseJson.app !== undefined && responseJson.app !== null && responseJson.app.length > 0) { // Base64 decode into json const foundapp = JSON.parse(atob(responseJson.app)) const selectedAppActions = selectedApp.actions === undefined || selectedApp.actions === null ? [] : selectedApp.actions if (foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > selectedAppActions.length) { if (select) { setSelectedApp(foundapp) } if (apps === undefined || apps === null || apps.length === 0) { console.log("LOAD APPS!") } for (var i = 0; i < apps.length; i++) { if (apps[i].id !== foundapp.id) { continue } apps[i] = foundapp setApps(apps) setFilteredApps(apps) // Update the local storage localStorage.setItem("apps", JSON.stringify(apps)) break } } if (cy !== undefined && cy !== null) { // Check if any apps in the workflow has cy.nodes().forEach((node) => { const data = node.data() if (data.app_id === foundapp.id) { if (data.name === "tmp" && data.parameters !== undefined && data.parameters !== null && data.parameters.length === 1 && data.parameters[0].name === "tmp" && foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > 0) { const startIndex = foundapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) const actionIndex = startIndex < 0 ? 0 : startIndex node.data("name", foundapp.actions[actionIndex].name) node.data("large_image", foundapp.large_image) node.data("parameters", foundapp.actions[actionIndex].parameters) node.data("finished", true) node.data("category", foundapp.categories !== null && foundapp.categories !== undefined && foundapp.categories.length > 0 ? foundapp.categories[0] : "") /* name: app.actions[actionIndex].name, label: actionLabel, app_name: app.name, app_version: app.app_version, app_id: app.id, sharing: app.sharing, private_id: app.private_id, description: description, environment: parsedEnvironments, errors: [], finished: false, id_: newNodeId, _id_: newNodeId, id: newNodeId, is_valid: true, type: actionType, parameters: parameters, isStartNode: false, large_image: app.large_image, run_magic_output: false, authentication: [], execution_variable: undefined, example: example, required_body_fields: app.actions[actionIndex].required_body_fields, authentication_id: authId, finished: false, template: app.template === true ? true : false, */ toast("REPLACING ACTIONS") } } }) //if (action.app_id === same && app.actions.length === 1 && app.actions[0].parameters.length === 1 && app.actions[0].parameters[0].name === "tmp") { } // FIXME: Add it to the existing list AND update the selected app } }) .catch((error) => { console.log(`Failed side-loading app ${appId}: ${error}`) }) } // Event for making sure app is correct useEffect(() => { if (selectedApp === undefined || selectedApp === null && selectedApp.app_name === undefined) { return } if (apps === undefined || apps === null || apps.length === 0) { return } // Handle the activation case, as they are NOT in the event management system yet if (selectedApp.actions === undefined || selectedApp.actions === null || selectedApp.actions.length > 1) { return } else { if (selectedApp.id !== undefined && selectedApp.id !== null && selectedApp.id.length > 0) { loadAppConfig(selectedApp.id, true) } } for (let appkey in apps) { const curapp = apps[appkey] if (curapp.name !== selectedApp.name) { continue } if (curapp.actions !== undefined && curapp.actions !== null && curapp.actions.length > selectedApp.actions.length) { var foundActionIndex = -1 for (let actionkey in curapp.actions) { const curaction = curapp.actions[actionkey] // First action with a label, as they are most used (typically) if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { foundActionIndex = actionkey break } } if (foundActionIndex >= 0) { var newaction = curapp.actions[foundActionIndex] setNewSelectedAction({ "target": { "value": newaction.name }, }) } setSelectedApp(curapp) } break } }, [selectedApp]) const [executionArgumentModalOpen, setExecutionArgumentModalOpen] = React.useState(false); // This should all be set once, not on every iteration // Use states and don't update lol const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; const appBarSize = isCloud ? 75 : 72; const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"]; const unloadText = "Are you sure you want to leave without saving (CTRL+S)?"; const classes = useStyles(); var [bodyWidth, bodyHeight] = useWindowSize() const cytoscapeWidth = isMobile ? bodyWidth - leftBarSize : bodyWidth - leftBarSize - 25 /* // Zoom testing to try autofixing for small screens if (document !== undefined && document !== null && !isMobile) { const currentZoom = document.body.style.zoom; if (bodyWidth < 1367 || bodyHeight < 769) { console.log("LOWER ZOOM") document.body.style.zoom = "80%" bodyWidth = bodyWidth*0.8 bodyHeight = bodyHeight*0.8 } else { console.log("RESET ZOOM") document.body.style.zoom = "100%" } } console.log("Width, height: ", bodyWidth, bodyHeight) */ //console.log("Mobile: ", isMobile, bodyWidth, bodyHeight) const [elements, setElements] = useState([]); const [loopRunning, setLoopRunning] = useState(false) var loopRunning2 = loopRunning const stop = () => { setLoopRunning(false) loopRunning2 = false } const start = () => { setLoopRunning(true) loopRunning2 = true } useEffect(() => { if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { setOriginalWorkflow(workflow) } // Special multi-workflow edgecase handler for events if (distributedFromParent === "" && suborgWorkflows === []) { } else { if (cy !== undefined) { cy.removeListener("select"); cy.removeListener("unselect"); cy.removeListener("add"); cy.removeListener("remove"); cy.removeListener("mouseover"); cy.removeListener("mouseout"); cy.removeListener("drag"); cy.removeListener("free"); cy.removeListener("cxttap"); setTimeout(() => { setupGraph(workflow) cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication); }); cy.on("select", "edge", (e) => onEdgeSelect(e)); cy.on("unselect", (e) => onUnselect(e)); cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); cy.on("remove", "node", (e) => onNodeRemoved(e)); cy.on("remove", "edge", (e) => onEdgeRemoved(e)); cy.on("mouseover", "edge", (e) => onEdgeHover(e)); cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); cy.on("mouseover", "node", (e) => onNodeHover(e)); cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); // Handles dragging cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); cy.on("cxttap", "node", (e) => onCtxTap(e)); cy.edgehandles({ handleNodes: (el) => { if (el.isNode() && el.data("buttonType") != "ACTIONSUGGESTION" && !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && el.data("type") !== "COMMENT") { return true } return false }, preview: true, toggleOffOnLeave: true, loopAllowed: function (node) { return false; }, }) }, 50) } } }, [workflow]) useEffect(() => { // Current variable + future state controlled // This is so that the loop can stop itself as well if (loopRunning && loopRunning2) { const intervalId = setInterval(() => { if (!loopRunning) { clearInterval(intervalId); } fetchUpdates() }, 3000) return () => clearInterval(intervalId); } }, [loopRunning]) // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. //const { start, stop } = useInterval({ // duration: 3000, // startImmediate: false, // callback: () => { // fetchUpdates(); // }, //}); const getAppDocs = (appname, location, version) => { fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, { headers: { Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status === 200) { //toast("Successfully GOT app "+appId) } else { //toast("Failed getting app"); } return response.json(); }) .then((responseJson) => { if (responseJson.success === true) { if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { setSelectedMeta(responseJson.meta) } if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { const imgRegex = / { toast(error.toString()); }); }; useEffect(() => { if (authenticationModalOpen === true && selectedAction.app_name !== undefined) { //console.log(selectedAction) //console.log("APP: ", selectedApp) if (selectedAction.documentation === undefined || selectedAction.documentation === null || selectedAction.documentation.length === 0) { // SelectedApp.documentation = Markdown? If so, it works // const apptype = selectedApp.generated === false ? "python" : "openapi" getAppDocs(selectedAction.app_name, apptype, selectedAction.app_version) } } }, [authenticationModalOpen]) const listOrgCache = (orgId) => { var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (orgId !== undefined && orgId !== null && orgId.length > 0) { headers["Org-Id"] = orgId } fetch(`${globalUrl}/api/v1/orgs/${orgId}/list_cache`, { method: "GET", headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { setListCache(responseJson); }) .catch((error) => { toast(error.toString()); }); }; const getWorkflowExecutionCount = (workflowId) => { fetch(`${globalUrl}/api/v1/workflows/${workflowId}/executions/count`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflow execution count: O!"); return; } else { return response.json(); } }) .then((responseJson) => { if (responseJson !== undefined) { setWorkflowExecutionCount(responseJson.count || 0); } }) .catch((error) => { toast(error.toString()); }); }; const getAvailableWorkflows = (trigger_index) => { fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!"); return; } return response.json(); }) .then((responseJson) => { if (responseJson !== undefined) { // Sets up subflow trigger with the right info if (trigger_index > -1) { var baseSubflow = {} const trigger = workflow.triggers[trigger_index]; if (trigger.parameters.length >= 3) { for (let paramkey in trigger.parameters) { const param = trigger.parameters[paramkey]; // User Input & Subflow nodes if (param.name === "workflow" || param.name === "subflow") { const paramIndex = param.name === "workflow" ? 0 : 5 if (workflow.triggers[trigger_index].parameters[paramIndex].value !== subworkflow.id) { if (param.value === workflow.id) { setSubworkflow(workflow); baseSubflow = workflow } else { const sub = responseJson.find((data) => data.id === param.value); if (sub !== undefined && subworkflow.id !== sub.id) { baseSubflow = sub setSubworkflow(sub); } } } } if (param.name === "startnode" && param.value !== undefined && param.value !== null) { if (Object.getOwnPropertyNames(baseSubflow).length > 0) { const foundAction = baseSubflow.actions.find(action => action.id === param.value) if (foundAction !== null && foundAction !== undefined) { setSubworkflowStartnode(foundAction); } } else { setSubworkflowStartnode(param.value); } } } } } if (workflows.length === 0) { //console.log("First request. Checking for parent trigger (if this is subflow") var parentworkflows = [] var parent_ids = [] for (let workflowkey in responseJson) { const innerworkflow = responseJson[workflowkey] for (let triggerkey in innerworkflow.triggers) { const trigger = innerworkflow.triggers[triggerkey] if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") { for (let paramkey in trigger.parameters) { const param = trigger.parameters[paramkey] if ((param.name === "workflow" || param.name === "subflow") && param.value === props.match.params.key && !parent_ids.includes(innerworkflow.id)) { parent_ids.push(innerworkflow.id) parentworkflows.push({ id: innerworkflow.id, name: innerworkflow.name, image: innerworkflow.image, }) } } } } } if (parentworkflows.length > 0) { setParentWorkflows(parentworkflows.filter(wf => wf.id !== props.match.params.key)) } } setWorkflows(responseJson); } }) .catch((error) => { //toast(error.toString()); console.log("Workflow error: ", error.toString()) }); }; function Heading(props) { const element = React.createElement( `h${props.level}`, { style: { marginTop: 40 } }, props.children ); return ( {props.level !== 1 ? ( ) : null} {element} ); } const generateApikey = () => { fetch(globalUrl + "/api/v1/generateapikey", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for APIKEY gen :O!"); } return response.json(); }) .then((responseJson) => { setUserSettings(responseJson); }) .catch((error) => { console.log("Apikey error: ", error); }); }; const getSettings = () => { fetch(globalUrl + "/api/v1/getsettings", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for get settings :O!"); } return response.json(); }) .then((responseJson) => { if ( responseJson.success === true && (responseJson.apikey === undefined || responseJson.apikey.length === 0 || responseJson.apikey === null) ) { generateApikey(); } if (responseJson.success === true) { setUserSettings(responseJson) } }) .catch((error) => { console.log("Settings error: ", error); }); }; const setNewAppAuth = (appAuthData, refresh) => { var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { headers["Org-Id"] = workflow.org_id } fetch(globalUrl + "/api/v1/apps/authentication", { method: "PUT", headers: headers, body: JSON.stringify(appAuthData), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting app auth :O!"); if (response.status === 400) { toast.error("Failed setting new auth. Please try again", { "autoClose": true, }) } } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { // Remove the timeout. Has to be clicked toast.error("Error: " + responseJson.reason, { "autoClose": false, }) } else { if (refresh === true) { getAppAuthentication(true, true, true) } else { getAppAuthentication(true, false) } setAuthenticationModalOpen(false) // Needs a refresh with the new authentication.. //toast("Successfully saved new app auth") if (configureWorkflowModalOpen === true) { setConfigureWorkflowModalOpen(false) setTimeout(() => { setConfigureWorkflowModalOpen(true) }, 1000) } } }) .catch((error) => { //toast(error.toString()); console.log("New auth error: ", error.toString()); }); }; const getWorkflowExecution = (id, execution_id, filter) => { var url = `${globalUrl}/api/v2/workflows/${id}/executions` var method = "GET" if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") { // Check for if (executionFilter !== undefined && executionFilter !== null && executionFilter.length > 0) { filter = executionFilter } else { filter = "ALL" } } var formattedBody = { method: method, headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", } if (filter !== "ALL") { formattedBody.method = "POST" formattedBody.body = JSON.stringify({ "status": filter, "workflow_id": id, }) url = `${globalUrl}/api/v1/workflows/search` } fetch(url, formattedBody) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson !== undefined && responseJson !== null && responseJson.runs !== undefined && responseJson.runs !== null) { responseJson.executions = responseJson.runs } if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) { // - means it's opposite const newkeys = sortByKey(responseJson.executions, "-started_at"); setWorkflowExecutions(newkeys); const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var tmpView = new URLSearchParams(cursearch).get("execution_id"); if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) { tmpView = execution_id; } // Compare with currently selected item if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { // Don't clean up if it's already open if (executionModalOpen === true) { return } const execution = responseJson.executions.find((data) => data.execution_id === tmpView); setExecutionModalOpen(true) if (execution !== null && execution !== undefined) { if (execution.execution_argument.includes("too large")) { setExecutionData({}); setExecutionRunning(true); setExecutionRequestStarted(false); } else { setExecutionData(execution); } setExecutionModalView(1); setExecutionRequest({ execution_id: execution.execution_id, authorization: execution.authorization, }); start(); //const newitem = removeParam("execution_id", cursearch); //navigate(curpath + newitem) } else { console.log("Couldn't find execution for execution ID. Retrying as user to get ", tmpView) //setExecutionRequestStarted(true); const cur_execution = { execution_id: tmpView, //authorization: data.authorization, } setExecutionRunning(true); setExecutionModalView(1); setExecutionRequest(cur_execution); start(); //const newitem = removeParam("execution_id", cursearch); //navigate(curpath + newitem) //setTimeout(() => { // stop() //}, 5000); } } } else { const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var tmpView = new URLSearchParams(cursearch).get("execution_id"); if (tmpView === undefined || tmpView === null || tmpView.length === 0) { const execution_id = tmpView; setExecutionModalView(1); setExecutionRequest({ execution_id: execution_id, }); start() } } }) .catch((error) => { //toast(error.toString()); console.log("Get execution error: ", error.toString()); }); }; const fetchUpdates = () => { fetch(globalUrl + "/api/v1/streams/results", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(executionRequest), credentials: "include", cors: "no-cors", }) .then((response) => { if (response.status !== 200) { stop(); setExecutionModalView(0); //toast("Failed loading the workflow run") console.log("Status not 200 for stream results :O!"); //const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; //const newitem = removeParam("execution_id", cursearch); //navigate(curpath + newitem) } return response.json(); }) .then((responseJson) => { handleUpdateResults(responseJson, executionRequest); }) .catch((error) => { console.log("Execution result Error: ", error); stop(); }); }; const abortExecution = () => { setExecutionRunning(false); fetch(globalUrl +"/api/v1/workflows/" +props.match.params.key +"/executions/" +executionRequest.execution_id +"/abort", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", } ) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for ABORT EXECUTION :O!"); } return response.json(); }) .catch((error) => { //toast(error.toString()); console.log("Abort error: ", error.toString()); }); }; const handleCommandSubmit = (trigger) => { if (trigger.trigger_type !== "PIPELINE") { toast("Unable to save the configuration"); return; } trigger.parameters = [] const command = document.getElementById('sigma')?.value if(command) { trigger.parameters.push({ name: "command", value: command }) } else { toast("Please enter the comamnd"); return; } // if (autoOffsetReset) { // trigger.parameters.push({ // name: "auto_offset_reset", // value: autoOffsetReset // }); // } setTenzirConfigModalOpen(false); }; const handleSubmit = (trigger) => { if (trigger.trigger_type !== "PIPELINE") { toast("Unable to save the configuration"); return; } if (selectedOption === "Kafka Queue") { trigger.parameters = [] const pipeline = document.getElementById('pipeline')?.value setTenzirConfigModalOpen(false); } else if (selectedOption === "Syslog listener") { trigger.parameters = [] const endpoint = document.getElementById('endpoint')?.value if(endpoint) { trigger.parameters.push({ name: "endpoint", value: endpoint }) } else { toast("Please enter your endpoint"); return; } } }; const handleColoring = (actionId, status, label) => { if (cy === undefined) { return } var currentnode = cy.getElementById(actionId); if (currentnode.length === 0) { return //continue; } currentnode = currentnode[0]; const outgoingEdges = currentnode.outgoers("edge"); const incomingEdges = currentnode.incomers("edge"); switch (status) { case "EXECUTING": currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); incomingEdges.addClass("success-highlight"); currentnode.addClass("executing-highlight"); break; case "SKIPPED": currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.removeClass("executing-highlight"); currentnode.addClass("skipped-highlight"); break; case "WAITING": currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.addClass("executing-highlight"); if (!visited.includes(label)) { if (executionRunning) { visited.push(label); setVisited(visited); } } // FIXME - add outgoing nodes to executing //const outgoingNodes = outgoingEdges.find().data().target if (outgoingEdges.length > 0) { outgoingEdges.addClass("success-highlight"); } break; case "SUCCESS": currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("executing-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.addClass("success-highlight"); incomingEdges.addClass("success-highlight"); outgoingEdges.addClass("success-highlight"); if (visited !== undefined && visited !== null && !visited.includes(label)) { if (executionRunning) { visited.push(label); setVisited(visited); } } // FIXME - add outgoing nodes to executing //const outgoingNodes = outgoingEdges.find().data().target if (outgoingEdges.length > 0) { for (let i = 0; i < outgoingEdges.length; i++) { const edge = outgoingEdges[i]; const targetnode = cy.getElementById(edge.data().target); if ( targetnode !== undefined && !targetnode.classes().includes("success-highlight") && !targetnode.classes().includes("failure-highlight") ) { targetnode.removeClass("not-executing-highlight"); targetnode.removeClass("success-highlight"); targetnode.removeClass("shuffle-hover-highlight"); targetnode.removeClass("failure-highlight"); targetnode.removeClass("awaiting-data-highlight"); targetnode.addClass("executing-highlight"); } } } break; case "FAILURE": //When status comes as failure, allow user to start workflow execution if (executionRunning) { setExecutionRunning(false); } currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.addClass("failure-highlight"); if (!visited.includes(label)) { //if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { // toast("Error for " + item.action.label + " with result " + item.result); //} visited.push(label); setVisited(visited); } break; case "AWAITING_DATA": currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.addClass("awaiting-data-highlight"); break; default: currentnode.removeClass("not-executing-highlight"); currentnode.removeClass("executing-highlight"); currentnode.removeClass("success-highlight"); currentnode.removeClass("failure-highlight"); currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.addClass("not-executing-highlight"); //console.log("DEFAULT -> Clearing!"); break; } } // Controls the colors and direction of execution results. // Style is in defaultCytoscapeStyle.js const handleUpdateResults = (responseJson, executionRequest) => { if (responseJson === undefined || responseJson === null || responseJson.success === false) { stop() return } //console.log(responseJson) // Loop nodes and find results // Update on every interval? idk ReactDOM.unstable_batchedUpdates(() => { if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) { // FIXME: If another is selected, don't edit.. // Doesn't work because this is some async garbage if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) { //console.log("Updating data!") setExecutionData(responseJson) } else { if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING" || responseJson.status === "FINISHED") { stop() } //console.log("NOT updating executiondata state."); return } } } if (responseJson.execution_id !== executionRequest.execution_id) { cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); return; } if (responseJson.results !== null && responseJson.results.length > 0) { // First clear current nodes if (responseJson.workflow.actions !== undefined && responseJson.workflow.actions !== null) { // In clearing of actions for (let actionKey in responseJson.workflow.actions) { var item = responseJson.workflow.actions[actionKey]; handleColoring(item.id, "", item.label) } } for (let resultKey in responseJson.results) { var item = responseJson.results[resultKey]; handleColoring(item.action.id, item.status, item.action.label) } } if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING") { stop(); if (executionRunning) { setExecutionRunning(false); } var curelements = cy.elements(); for (let i = 0; i < curelements.length; i++) { if (curelements[i].classes().includes("executing-highlight")) { curelements[i].removeClass("executing-highlight"); curelements[i].addClass("failure-highlight"); } } getWorkflowExecution(props.match.params.key, ""); } else if (responseJson.status === "FINISHED") { setExecutionRunning(false); stop(); getWorkflowExecution(props.match.params.key, ""); setUpdate(Math.random()); } }) }; var streamDisabled2 = false const sendStreamRequest = (body) => { //console.log("Stream not activated yet.") if (!isCloud) { return } if (streamDisabled) { return } // Session may be important here huh body.user_id = userdata.id //const url = ${globalUrl}/api/v1/workflows/${props.match.params.key}/stream //const streamUrl = "http://localhost:5002" //console.log("Stream request: ", body) const streamUrl = "https://stream.shuffler.io" const url = `${streamUrl}/api/v1/workflows/${props.match.params.key}/stream` var parsedbody = body try { parsedbody = JSON.stringify(body) } catch (e) { console.log("Error parsing body for stream: ", e) } fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: parsedbody, credentials: "include", }) .then((response) => { setSavingState(0); if (response.status !== 200) { setStreamDisabled(true) streamDisabled2 = true //console.log("Status not 200 for stream :O!"); } return response.json(); }) .then((responseJson) => { //console.log("Stream resp: ", responseJson) }) .catch((error) => { console.log("Stream send error: ", error.toString()) setStreamDisabled(true) streamDisabled2 = true }) } const saveWorkflow = (curworkflow, executionArgument, startNode, duplicationOrg) => { var success = false; if (isCloud && !isLoggedIn) { console.log("Should redirect to register with redirect.") setTimeout(() => { toast("You may not have access to this workflow.") //window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` window.location.href = `/workflows` }, 2500) return } if (curworkflow === undefined || curworkflow === null) { console.log("No workflow during save") return } if (curworkflow.actions === undefined || curworkflow.actions === null || curworkflow.actions.length === 0) { console.log("Can't save without actions") return } setSavingState(2); // This might not be the right course of action, but seems logical, as items could be running already // Makes it possible to update with a version in current render stop(); var useworkflow = workflow; if (curworkflow !== undefined) { useworkflow = curworkflow; } var cyelements = [] if (cy !== undefined && cy !== null) { cyelements = cy.elements() } var newActions = []; var newTriggers = []; var newBranches = []; var newVBranches = []; var newComments = []; for (let cyelementsKey in cyelements) { if (cyelements[cyelementsKey].data === undefined) { continue; } var type = cyelements[cyelementsKey].data()["type"] if (type === undefined) { if (cyelements[cyelementsKey].data().source === undefined || cyelements[cyelementsKey].data().target === undefined) { continue } // Get the parent item var source_attachment = "" const branchSource = cy.getElementById(cyelements[cyelementsKey].data().source) if (branchSource === undefined || branchSource === null) { } else { const branchSourceData = branchSource.data() if (branchSourceData !== undefined && branchSourceData !== null && branchSourceData.attachedTo !== undefined) { source_attachment = branchSourceData.attachedTo // Check if it's the 'else' or not based on uuidv5 const else_attachment = uuidv5(source_attachment, uuidv5.URL) if (else_attachment === branchSourceData.id) { source_attachment = source_attachment+"-else" } console.log("Source parent: ", source_attachment) } } var parsedElement = { id: cyelements[cyelementsKey].data().id, source_id: cyelements[cyelementsKey].data().source, destination_id: cyelements[cyelementsKey].data().target, conditions: cyelements[cyelementsKey].data().conditions, decorator: cyelements[cyelementsKey].data().decorator, source_parent: source_attachment, } if (parsedElement.decorator) { newVBranches.push(parsedElement) } else { newBranches.push(parsedElement) } } else { if (type === "ACTION") { const cyelement = cyelements[cyelementsKey].data(); const elementid = cyelement.id === undefined || cyelement.id === null ? cyelement["_id"] : cyelement.id; var curworkflowAction = useworkflow.actions.find( (a) => a !== undefined && (a["id"] === elementid || a["_id"] === elementid) ); if (curworkflowAction === undefined) { curworkflowAction = cyelements[cyelementsKey].data(); } curworkflowAction.position = cyelements[cyelementsKey].position(); // workaround to fix some edgecases if ( curworkflowAction.parameters === "" || curworkflowAction.parameters === null ) { curworkflowAction.parameters = []; } if ( curworkflowAction.example === undefined || curworkflowAction.example === "" || curworkflowAction.example === null ) { if (cyelements[cyelementsKey].data().example !== undefined) { curworkflowAction.example = cyelements[cyelementsKey].data().example; } } // Override just in this place curworkflowAction.errors = []; curworkflowAction.isValid = true; // Cleans up OpenAPI items var newparams = []; for (let parametersKey in curworkflowAction.parameters) { const thisitem = curworkflowAction.parameters[parametersKey]; if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { continue; } if (thisitem.value !== undefined && thisitem.value !== null && Array.isArray(thisitem.value)) { thisitem.value = thisitem.value.join(",") } newparams.push(thisitem); } curworkflowAction.parameters = newparams; newActions.push(curworkflowAction); } else if (type === "TRIGGER") { if (useworkflow.triggers === undefined || useworkflow.triggers === null) { useworkflow.triggers = []; } var curworkflowTrigger = useworkflow.triggers.find( (a) => a.id === cyelements[cyelementsKey].data()["id"] ); if (curworkflowTrigger === undefined) { curworkflowTrigger = cyelements[cyelementsKey].data(); } curworkflowTrigger.position = cyelements[cyelementsKey].position(); if (curworkflowTrigger.canConnect === false) { continue } newTriggers.push(curworkflowTrigger); } else if (type === "COMMENT") { if (useworkflow.comments === undefined || useworkflow.comments === null) { useworkflow.comments = []; } var curworkflowComment = useworkflow.comments.find( (a) => a.id === cyelements[cyelementsKey].data()["id"] ) if (curworkflowComment === undefined) { curworkflowComment = cyelements[cyelementsKey].data(); try { curworkflowComment.position.x = parseInt(curworkflowComment.position.x) } catch (e) { console.log("Failed to parse position Y of comment: ", curworkflowComment.position.x) } try { curworkflowComment.position.y = parseInt(curworkflowComment.position.y) } catch (e) { console.log("Failed to parse position Y of comment: ", curworkflowComment.position.y) } } const parsedHeight = parseInt(curworkflowComment["height"]) if (!isNaN(parsedHeight)) { curworkflowComment.height = parsedHeight } else { curworkflowComment.width = 150 } const parsedWidth = parseInt(curworkflowComment["width"]) if (!isNaN(parsedWidth)) { curworkflowComment.width = parsedWidth } else { curworkflowComment.width = 200 } curworkflowComment.position = cyelements[cyelementsKey].position(); //console.log(curworkflowComment) newComments.push(curworkflowComment); } else { toast("No handler for type: " + type); } } } if (userediting === true) { useworkflow.user_editing = true } useworkflow.actions = newActions; useworkflow.triggers = newTriggers; useworkflow.branches = newBranches; useworkflow.comments = newComments; useworkflow.visual_branches = newVBranches; // Errors are backend defined useworkflow.errors = []; useworkflow.previously_saved = true; // Find the startnode in actions /* var foundStartNode = useworkflow.actions.find((a) => a.is_start_node === true) console.log("Discovered startnode: ", foundStartNode) if ((foundStartNode === undefined || foundStartNode === null) && useworkflow.actions.length > 0) { // Set a startnode useworkflow.actions[0].is_start_node = true useworkflow.start = useworkflow.actions[0].id } */ if (cy !== undefined) { // scale: 0.3, // bg: "#27292d", const cyImageData = cy.png({ output: "base64uri", maxWidth: 480, maxHeight: 270, }) if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { useworkflow.image = cyImageData } } if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { useworkflow.id = props.match.params.key } var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (useworkflow.org_id !== undefined && useworkflow.org_id !== null && useworkflow.org_id.length > 0) { headers["Org-Id"] = useworkflow.org_id } // Realtime makes the workflow if it doesn't exist /* if (duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { headers["Org-Id"] = duplicationOrg } */ setLastSaved(true); fetch(`${globalUrl}/api/v1/workflows/${useworkflow.id}`, { method: "PUT", headers: headers, body: JSON.stringify(useworkflow), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); } else { if (distributedFromParent === "" && suborgWorkflows === []) { } else { getChildWorkflows(useworkflow.id) } } return response.json(); }) .then((responseJson) => { if (useworkflow.id === originalWorkflow.id && duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { //duplicateParentWorkflow(useworkflow, duplicationOrg, true) duplicateParentWorkflow(useworkflow, duplicationOrg, true) } if (executionArgument !== undefined && startNode !== undefined) { //console.log("Running execution AFTER saving"); setSavingState(0); executeWorkflow(executionArgument, startNode, true); return; } if (!responseJson.success) { setSavingState(0); console.log(responseJson); if (responseJson.reason !== undefined && responseJson.reason !== null) { toast("Failed to save: " + responseJson.reason); } else { toast("Failed to save. Please contact your support@shuffler.io or your local admin if this is unexpected.") } } else { setSavingState(1); sendStreamRequest({ "item": "workflow", "type": "save", "id": workflow.id, }) if ( responseJson.new_id !== undefined && responseJson.new_id !== null ) { window.location.pathname = "/workflows/" + responseJson.new_id; } success = true; if (responseJson.errors !== undefined) { workflow.errors = responseJson.errors; if (responseJson.errors.length === 0) { workflow.isValid = true; workflow.is_valid = true; const cyelements = cy.elements(); for (let i = 0; i < cyelements.length; i++) { //cyelements[i].removeStyle(); cyelements[i].data().is_valid = true; cyelements[i].data().errors = []; } for (let actionkey in workflow.actions) { workflow.actions[actionkey].is_valid = true; workflow.actions[actionkey].errors = []; } } setWorkflow(workflow); } setTimeout(() => { setSavingState(0); }, 1500); getRevisionHistory(useworkflow.id) } }) .catch((error) => { setSavingState(0); setExecutionRequestStarted(false) console.log("Save workflow error: ", error.toString()); toast.warn("Failed to save the workflow. Is the network down?") }); if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { setOriginalWorkflow(useworkflow) } return success }; const monitorUpdates = () => { var firstnode = cy.getElementById(workflow.start); if (firstnode.length === 0) { var found = false; for (let actionkey in workflow.actions) { if (workflow.actions[actionkey].isStartNode) { console.log("Updating startnode"); workflow.start = workflow.actions[actionkey].id; firstnode = cy.getElementById(workflow.actions[actionkey].id); found = true; break; } } if (!found) { return false; } } cy.elements().removeClass( "success-highlight failure-highlight executing-highlight" ); firstnode[0].addClass("executing-highlight"); return true; }; const executeWorkflow = (executionArgument, startNode, hasSaved) => { if (hasSaved === false) { setExecutionRequestStarted(true) saveWorkflow(workflow, executionArgument, startNode); //console.log("FIXME: Might have forgotten to save before executing."); return; } if (workflow.public) { toast("Save it to get a new version"); } var returncheck = monitorUpdates(); if (!returncheck) { toast("No startnode set."); return; } ReactDOM.unstable_batchedUpdates(() => { setVisited([]) setExecutionRequest({}) stop() // FIXME: Check if any node contains $exec in a param // If they do, show a popup asking if they want to execute it without an execution argument, or to use a previous one if (executionArgument === undefined || executionArgument === null || executionArgument.length === 0) if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { var foundmissing = false for (let actionkey in workflow.actions) { if (workflow.actions[actionkey].parameters === undefined || workflow.actions[actionkey].parameters === null || workflow.actions[actionkey].parameters.length === 0) { continue } for (let paramkey in workflow.actions[actionkey].parameters) { const param = workflow.actions[actionkey].parameters[paramkey] if (param.value === undefined || param.value === null || param.value.length === 0) { continue } if (param.value.indexOf("$exec") !== -1) { foundmissing = true break } } if (foundmissing) { break } } if (foundmissing) { //toast("This workflow contains a node that requires an execution argument. Please provide one.") if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { setExecutionRequestStarted(false) setExecutionArgumentModalOpen(true) return } if (workflowExecutions.length > 0) { setExecutionRequestStarted(false) setExecutionArgumentModalOpen(true) return } } } var curelements = cy.elements(); for (let i = 0; i < curelements.length; i++) { curelements[i].addClass("not-executing-highlight"); } var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { headers["Org-Id"] = workflow.org_id } const data = { execution_argument: executionArgument, start: startNode }; fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`, { method: "POST", headers: headers, credentials: "include", body: JSON.stringify(data), } ) .then((response) => { setExecutionRequestStarted(false) if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { //toast("Failed to start: " + responseJson.reason); toast(responseJson.reason); //toast.error(responseJson.reason); setExecutionRunning(false); setExecutionRequestStarted(false); stop(); for (let i = 0; i < curelements.length; i++) { curelements[i].removeClass("not-executing-highlight"); } return; } else { if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) { navigate(`?execution_id=${responseJson.execution_id}`) } setExecutionRunning(true); setExecutionRequestStarted(false); } if ( responseJson.execution_id === "" || responseJson.execution_id === undefined || responseJson.authorization === "" || responseJson.authorization === undefined ) { toast("Something went wrong during execution startup"); console.log("BAD RESPONSE FOR EXECUTION: ", responseJson); setExecutionRunning(false); setExecutionRequestStarted(false); stop(); for (let i = 0; i < curelements.length; i++) { curelements[i].removeClass("not-executing-highlight"); } return; } setExecutionRequest({ execution_id: responseJson.execution_id, authorization: responseJson.authorization, }); setExecutionData({}); setExecutionModalOpen(true); setExecutionModalView(1); start(); }) .catch((error) => { //toast(error.toString()); setExecutionRequestStarted(false) console.log("Execute workflow err: ", error.toString()); toast.warn("Failed to run the workflow. Is the network down?") }); }) }; // This can be used to only show prioritzed ones later // Right now, it can prioritize authenticated ones //"Testing", // // const getAuthGroups = (orgId) => { setAuthGroups([]) var headers = { "content-type": "application/json", "accept": "application/json", } if (orgId !== undefined && orgId !== null && orgId.length > 0) { headers["Org-Id"] = orgId } fetch(globalUrl + "/api/v1/authentication/groups", { method: "GET", headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for app auth :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success === true) { setAuthGroups(responseJson.data) } else { console.log("AppAuth group loading error: " + responseJson.reason); } }) .catch((error) => { setAuthGroups([]); console.log("AppAuth group loading error: " + error.toString()); }) } const getAppAuthentication = (reset, updateAction, closeMenu, orgId) => { var headers = { "content-type": "application/json", "accept": "application/json", } if (orgId !== undefined && orgId !== null && orgId.length > 0) { headers["Org-Id"] = orgId } fetch(globalUrl + "/api/v1/apps/authentication", { method: "GET", headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for app auth :O!"); } return response.json(); }) .then((responseJson) => { var shouldClose = false if (responseJson.success) { getAuthGroups(orgId) var newauth = []; for (let authkey in responseJson.data) { if (responseJson.data[authkey].defined === false) { continue; } newauth.push(responseJson.data[authkey]); } setAppAuthentication(newauth); if (cy !== undefined) { // Remove the old listener for select, run with new one cy.removeListener("select"); cy.on("select", "node", (e) => onNodeSelect(e, newauth)); cy.on("select", "edge", (e) => onEdgeSelect(e)); } if (updateAction === true) { if (selectedApp.authentication.required) { // Setup auth here :) var appUpdates = false; const authenticationOptions = []; var tmpAuth = JSON.parse(JSON.stringify(newauth)); var latest = 0; for (let authkey in tmpAuth) { var item = tmpAuth[authkey]; //console.log("Got auth: ", item); const newfields = {}; for (let filterkey in item.fields) { newfields[item.fields[filterkey].key] = item.fields[filterkey].value; } item.fields = newfields; const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) if (itemname === appname) { authenticationOptions.push(item); // Always becoming the last one if (item.edited > latest) { latest = item.edited; selectedAction.selectedAuthentication = item; for (let actionkey in workflow.actions) { const actionAppname = workflow.actions[actionkey].app_name.toLowerCase().replaceAll(" ", "_", -1) if (actionAppname === appname) { workflow.actions[actionkey].selectedAuthentication = item; workflow.actions[actionkey].authentication_id = item.id; appUpdates = true; } } } else { //console.log("Not newer: ", item.edited, " vs ", latest) } } else { //console.log("Appname is wrong: ", appname, " vs ", itemname) } } console.log("auth options: ", authenticationOptions) selectedAction.authentication = authenticationOptions if (selectedAction.selectedAuthentication === null || selectedAction.selectedAuthentication === undefined || selectedAction.selectedAuthentication.length === "") { selectedAction.selectedAuthentication = {} } if (appUpdates === true) { console.log("Closing auth modal: Success") setAuthenticationModalOpen(false); setSelectedAction(selectedAction); setWorkflow(workflow); saveWorkflow(workflow); toast("Added and updated authentication!"); shouldClose = true } else { console.log("Closing auth modal? FAIL") toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); shouldClose = false } } else { toast("No authentication to update"); } } else { shouldClose = true } } else { setAppAuthentication([]) shouldClose = true } // Auto-closing if changes were made if (closeMenu === true && shouldClose === true) { setAuthenticationModalOpen(false); } }) .catch((error) => { setAppAuthentication([]); //toast("Auth loading error: " + error.toString()); console.log("AppAuth error: " + error.toString()); }); }; const getApps = () => { fetch(globalUrl + "/api/v1/apps", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps. Setting to pretend apps!"); const pretend_apps = [{ "name": "TBD", "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", "version": "TBD", "large_image": "", }] setAppsLoaded(true) setFilteredApps(pretend_apps) setApps(Array.prototype.concat.apply(pretend_apps, triggers)) setPrioritizedApps(pretend_apps) if (isLoggedIn) { toast("Something went wrong while loading apps. Please refresh the window to try again.") } return } //console.log("Apps loaded. JSON decoding next") return response.json() }) .then((responseJson) => { if (responseJson === null) { console.log("No response") const pretend_apps = [{ "name": "TBD", "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", "version": "TBD", "large_image": "", }] setAppsLoaded(true) setFilteredApps(pretend_apps) setApps(Array.prototype.concat.apply(pretend_apps, triggers)) setPrioritizedApps(pretend_apps) return } if (responseJson.success === false) { return } // Used for e.g. Liquid testing const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") if (foundTools !== undefined && foundTools !== null) { setToolsApp(foundTools) } // Set localstorage for the apps in the "apps" key setApps(Array.prototype.concat.apply(responseJson, triggers)) if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { try { localStorage.setItem("apps", JSON.stringify(responseJson)) } catch (e) { console.log("Failed to set apps in localstorage: ", e) } } var handledPrioritizedApps = responseJson.filter((app) => internalIds.includes(app.name.toLowerCase())); handledPrioritizedApps = [].concat(integrationApps, handledPrioritizedApps) if (isCloud) { setFilteredApps(responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase()))) setPrioritizedApps(handledPrioritizedApps) } else { const tmpFiltered = responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase())) setFilteredApps(tmpFiltered) setPrioritizedApps(handledPrioritizedApps) } setAppsLoaded(true) // Remove all cytoscape triggers first? if (cy !== undefined && cy !== null) { cy.removeListener("select") } // Re-adding cytoscape triggers if (cy !== undefined && cy !== null) { cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication) }) } }) .catch((error) => { console.log("App loading error: " + error.toString()) setAppsLoaded(true) //toast("App loading error: "+error.toString()) }); }; // Searhc by username, userId, workflow, appId should all work const getUserProfile = (username, rerun) => { fetch(`${globalUrl}/api/v1/users/creators/${username}`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { console.log("Found creator: ", responseJson) setCreatorProfile(responseJson) } else { console.log("Couldn't find the creator profile (rerun?): ", responseJson, rerun) // If the current user is any of the Shuffle Creators // AND the workflow doesn't have an owner: allow editing. // else: Allow suggestions? //console.log("User: ", userdata) //if (rerun !== true) { // getUserProfile(userdata.id, true) //} } }) .catch((error) => { console.log("Get userprofile error: ", error); }) } const getFiles = (orgId) => { var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (orgId !== undefined && orgId !== null && orgId.length > 0) { headers["Org-Id"] = orgId } fetch(globalUrl + "/api/v1/files", { method: "GET", headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { //console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { if (responseJson.files !== undefined && responseJson.files !== null) { setFiles(responseJson); } else { setFiles({ "namespaces": [ "default" ] }); } if ( responseJson.namespaces !== undefined && responseJson.namespaces !== null ) { //setFileNamespaces(responseJson.namespaces); } }) .catch((error) => { //toast(error.toString()); console.log("Error loading files: ", error) }); }; const onChunkedResponseComplete = (result) => { // Dont return until in 5 seconds without setTimeout } const onChunkedResponseError = (err) => { if (streamDisabled) { return } } const uuidToHSV = (uuid) => { // Convert the UUID to a hexadecimal string without dashes const uuidHex = uuid.replace(/-/g, ""); // Take the first 6 characters of the hexadecimal UUID as the seed const seed = parseInt(uuidHex.slice(0, 6), 16); // Normalize the seed to a value between 0 and 1 const normalizedSeed = seed / 0xFFFFFF; // 0xFFFFFF is the maximum possible value with 6 hexadecimal characters // Use the normalized seed to generate HSV values const hue = normalizedSeed; // Hue value between 0 and 1 const saturation = 0.8; // You can adjust the saturation value as desired (between 0 and 1) const value = 0.8; // You can adjust the value/brightness as desired (between 0 and 1) // Convert HSV to RGB const rgb = HSVtoRGB(hue, saturation, value); // Scale the RGB values to the 0-255 range const scaledRGB = rgb.map(val => Math.round(val * 255)); return scaledRGB; } // HSV to RGB conversion function const HSVtoRGB = (h, s, v) => { const h_i = Math.floor(h * 6); const f = h * 6 - h_i; const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); switch (h_i % 6) { case 0: return [v, t, p]; case 1: return [q, v, p]; case 2: return [p, v, t]; case 3: return [p, q, v]; case 4: return [t, p, v]; case 5: return [v, p, q]; default: return [0, 0, 0]; } } const rgbToHex = (rgb) => { // Ensure that each component is in the valid range (0-255) const r = Math.max(0, Math.min(255, rgb[0])); const g = Math.max(0, Math.min(255, rgb[1])); const b = Math.max(0, Math.min(255, rgb[2])); // Convert the RGB values to hexadecimal and pad with zeros if needed const rHex = r.toString(16).padStart(2, "0"); const gHex = g.toString(16).padStart(2, "0"); const bHex = b.toString(16).padStart(2, "0"); // Combine the hexadecimal values to create the final color code const hexColor = `#${rHex}${gHex}${bHex}`; return hexColor.toUpperCase(); // Optionally, make the result uppercase } const getUserColor = (user_id) => { //return "#ffffff" return rgbToHex(uuidToHSV(user_id)) } const hoverNode = (chunkJson) => { // Find the node if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } var node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } const color = getUserColor(chunkJson.user_id) const parsedStyle = { "border-width": "6px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) // Wait 3 seconds and remove it setTimeout(() => { const parsedStyle = { "border-width": "1px", "font-size": "18px", "border-color": "#81c784", } node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) }, 3000) } const moveNode = (chunkJson) => { // Find the node if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } var node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } console.log("Moving node: ", node, chunkJson) // Find nodes attached to the node node.position({ x: chunkJson.location.x, y: chunkJson.location.y }) const connectedNodes = cy.filter('node[attachedTo = "'+chunkJson.id+'"]') if (connectedNodes === undefined || connectedNodes === null) { console.log("Connected nodes is undefined or null") } else { console.log("Connected nodes: ", connectedNodes) connectedNodes.remove() } const color = getUserColor(chunkJson.user_id) const parsedStyle = { "border-width": "11px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) // Wait 3 seconds and remove it setTimeout(() => { const parsedStyle = { "border-width": "1px", "font-size": "18px", "border-color": "#81c784", } node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) }, 3000) } const hoverEdge = (chunkJson) => { // Find the node if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } var node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } const color = getUserColor(chunkJson.user_id) const parsedStyle = { "line-gradient-stop-positions": ["0.0", "100"], "line-gradient-stop-colors": [color, color], } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) // Wait 3 seconds and remove it setTimeout(() => { const parsedStyle = { "line-gradient-stop-positions": ["0.0", "100"], "line-gradient-stop-colors": ["grey", "grey"], } node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) }, 3000) } const selectNode = (chunkJson) => { if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } var node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } const color = getUserColor(chunkJson.user_id) const parsedStyle = { "border-width": "11px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) setTimeout(() => { const parsedStyle = { "border-width": "11px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) }, 3000) } const unselectNode = (chunkJson) => { if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } var node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } const color = getUserColor(chunkJson.user_id) const parsedStyle = { "border-width": "1px", "font-size": "18px", "border-color": "#81c784", } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) } const addNode = (chunkJson) => { if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } const node = cy.getElementById(chunkJson.id) if (node !== undefined && node !== null) { return } const color = getUserColor(chunkJson.user_id) // Create the node and add to cytoscape const data = chunkJson.data const nodeData = { group: "nodes", data: chunkJson.data, position: { x: data.x, y: data.y }, } cy.add(nodeData) // Wait 100ms then add a style for it setTimeout(() => { const node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null during auto add from other user") return } const parsedStyle = { "border-width": "11px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) }, 100) } const removeNodeStream = (chunkJson) => { if (cy === undefined || cy === null) { console.log("Cy is undefined or null") return } const node = cy.getElementById(chunkJson.id) if (node === undefined || node === null) { console.log("Node is undefined or null") return } const color = getUserColor(chunkJson.user_id) // Animate node, then delete 1 sec later const parsedStyle = { "border-width": "11px", "border-opacity": ".7", "font-size": "25px", "border-color": color, } const animationDuration = 150 node.animate( { style: parsedStyle, }, { duration: animationDuration, } ) setTimeout(() => { node.remove() }, 1000) } const processChunkedResponse = async (response) => { console.log("In process resp!") var text = ''; var reader = response.body.getReader() var decoder = new TextDecoder(); const appendChunks = (result) => { var chunk = decoder.decode(result.value || new Uint8Array, {stream: !result.done}); if (chunk === undefined || chunk === null) { console.log("Chunk is undefined or null") } // Try chunk JSON loading try { var chunkJson = JSON.parse(chunk) if (chunkJson.success === false) { console.log("Chunk failed: ", chunkJson) if (!streamDisabled) { setStreamDisabled(true) streamDisabled2 = true } return } if (chunkJson.item !== undefined && chunkJson.item !== null && chunkJson.item !== "") { if (chunkJson.item === "node") { if (chunkJson.type === "move") { moveNode(chunkJson) } else if (chunkJson.type === "hover") { hoverNode(chunkJson) } else if (chunkJson.type === "select") { selectNode(chunkJson) } else if (chunkJson.type === "unselect") { unselectNode(chunkJson) } else if (chunkJson.type === "add") { addNode(chunkJson) } else if (chunkJson.type === "remove") { removeNodeStream(chunkJson) } } else if (chunkJson.item === "edge") { if (chunkJson.type === "hover") { // Same as node function? //hoverEdge(chunkJson) } } } } catch (e) { console.log("Chunk JSON error: ", e) if (!streamDisabled) { setStreamDisabled(true) streamDisabled2 = true } return } //data.push(chunk) //setData(data) //setUpdate(Math.random()); //console.log('got chunk of', chunk.length, 'bytes. Value: ', chunk) text += chunk; //console.log('text so far is', text.length, 'bytes if (result.done) { console.log('returning') return text; } else { return readChunk() } } const readChunk = () => { return reader.read().then(appendChunks); } return readChunk(); } async function fetchWithTimeout(resource, options = {}) { const { timeout = 8000 } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const response = await fetch(resource, { ...options, signal: controller.signal }); clearTimeout(id); return response; } const startWorkflowStream = async (workflowId) => { if (!isCloud) { console.log("Not cloud, not starting workflow stream") return } if (streamDisabled) { console.log("Stream listener disabled") return } const timeout = 60000 //const url = `${globalUrl}/api/v1/workflows/${workflowId}/stream` //const streamUrl = "https://shuffle-streaming-backend-stbuwivzoq-ew.a.run.app" // const streamUrl = "https://stream.shuffler.io" const url = `${streamUrl}/api/v1/workflows/${workflowId}/stream` while (true) { if (streamDisabled === true || streamDisabled2 === true) { console.log("Stream disabled, breaking") break } // Wait 1 second before next request just in case of timeouts await new Promise(r => setTimeout(r, 1000)); await fetchWithTimeout(url, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", timeout: timeout, }) .then(processChunkedResponse) .then(onChunkedResponseComplete) .catch(onChunkedResponseError) } } const [usedSubflowApps, setUsedSubflowApps] = React.useState([]); const getWorkflowApps = (workflow_id) => { let apps = [] if (workflow_id === "") { console.log("workflow_id is empty"); return {}; } fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!"); } return response.json(); }) .then((responseJson) => { for (let index in responseJson.actions) { apps.push(responseJson.actions[index]); } console.log("Setting used subflow apps: ", apps) setUsedSubflowApps(apps); return apps }) .catch((error) => { console.log("Get workflow apps error: ", error); }); return apps }; const getChildWorkflows = (parentWorkflowId) => { //toast("Loading child workflows 1 (should be 2)") /* if (originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0) { return } */ const orgId = originalWorkflow.org_id === undefined || originalWorkflow.org_id === null || originalWorkflow.org_id === "" ? "" : originalWorkflow.org_id //toast("Loading child workflows 2: " + orgId) fetch(`${globalUrl}/api/v1/workflows/${parentWorkflowId}/child_workflows`, { method: "GET", headers: { "Content-Type": "application/json", "Accept": "application/json", "Org-Id": orgId, }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { setSuborgWorkflows(responseJson) } }) .catch((error) => { console.log("Get child workflows error: ", error); }) } const getWorkflow = (workflow_id, sourcenode) => { fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!"); if (response.status >= 500) { toast("Something went wrong while loading the workflow. Please reload.") } else { // Check for execution_id in URL // don't redirect if it exists const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var execFound = new URLSearchParams(cursearch).get("execution_id"); var sessionToken = new URLSearchParams(cursearch).get("session_token"); if (execFound === null && sessionToken === null) { toast(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds..`) setTimeout(() => { window.location.pathname = "/workflows"; }, 2000); } else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { toast(`Injecting session token and reloading workflow..`) setTimeout(() => { setCookie("session_token", sessionToken, { path: "/" }); window.location.href = "https://shuffler.io/workflows/3abdfb21-b40f-4e50-b855-ac0d62f83cbe"; }, 2000) } } } // Read text from stream //return response.text(); return response.json(); }) .then((responseJson) => { // Load as JSON if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.id !== workflow_id) { toast("Workflow ID mismatch. Redirecting to your workflow") navigate(`/workflows/${responseJson.id}`) } if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow !== "") { setDistributedFromParent(responseJson.parentorg_workflow) } if (responseJson.childorg_workflow_ids !== undefined && responseJson.childorg_workflow_ids !== null && responseJson.childorg_workflow_ids.length > 0) { getChildWorkflows(responseJson.id) } else if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow.length > 0) { getChildWorkflows(responseJson.parentorg_workflow) } // Not sure why this is necessary. if (responseJson.isValid === undefined) { responseJson.isValid = true; } if (responseJson.errors === undefined) { responseJson.errors = []; } if (responseJson.actions === undefined || responseJson.actions === null) { responseJson.actions = []; } if (responseJson.triggers === undefined || responseJson.triggers === null) { responseJson.triggers = []; } if (responseJson.org_id !== undefined && responseJson.org_id !== null) { listOrgCache(responseJson.org_id) } if (responseJson.sharing !== undefined && responseJson.sharing !== null && (responseJson.sharing === "form" || responseJson.sharing === "forms")) { if (responseJson.actions === undefined || responseJson.actions === null || responseJson.actions.length === 0) { navigate("/forms/" + responseJson.id) toast("Redirecting to Form from Workflow") } } // Wait for this to finish fetchRecommendations(responseJson) if (responseJson.public) { setAppAuthentication([]) console.log("RESP: ", responseJson) if (Object.getOwnPropertyNames(creatorProfile).length === 0) { //getUserProfile("frikky") getUserProfile(responseJson.id, false) } //{appGroup.map((data, index) => { //const [appGroup, setAppGroup] = React.useState([]); var appsFound = [] for (let actionkey in responseJson.actions) { const parsedAction = responseJson.actions[actionkey] if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") { continue } if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ appsFound.push(parsedAction) } } setAppGroup(appsFound) appsFound = [] for (let triggerkey in responseJson.triggers) { const parsedAction = responseJson.triggers[triggerkey] if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ appsFound.push(parsedAction) } } setTriggerGroup(appsFound) setWorkflows([responseJson]) } else { getAppAuthentication(); getEnvironments(); getSettings(); getFiles() getWorkflowExecution(props.match.params.key, ""); getAvailableWorkflows(-1); } // Appends SUBFLOWS. Does NOT run during normal grabbing of workflows. if (sourcenode.id !== undefined) { var nodefound = false; var target = sourcenode.parameters.find((item) => item.name === "startnode"); console.log("Got rightclick target: ", target) if (target === undefined || target === null) { target = { "name": "startnode", "value": responseJson.start } } console.log(sourcenode.parameters); console.log(target); const target_id = target === undefined ? "" : target.value; const actions = responseJson.actions.map((action) => { const node = { group: "nodes", }; // Set it dynamically? node.position = { x: sourcenode.position.x + action.position.x, y: sourcenode.position.y + action.position.y, }; node.data = action; node.data.canConnect = false node.data.is_valid = true node.data.isValid = true node.data._id = action["id"]; node.data.type = "ACTION"; node.data.source_workflow = responseJson.id; if (action.id === target_id) { nodefound = true; } if (responseJson.public) { node.data.is_valid = true node.is_valid = true } var example = ""; if ( action.example !== undefined && action.example !== null && action.example.length > 0 ) { example = action.example; } node.data.example = example; return node; }); var triggers = responseJson.triggers.map((trigger) => { const node = {}; console.log("Only add workflow: ", trigger.app_name) if (trigger.app_name !== "Shuffle Workflow" && trigger.app_name !== "User Input") { return null } node.position = trigger.position; node.data = trigger; node.data.canConnect = false node.data.id = trigger["id"]; node.data._id = trigger["id"]; node.data.type = "TRIGGER"; return node; }); triggers = triggers.filter((trigger) => trigger !== null); const insertedNodes = [].concat(actions, triggers); var edges = responseJson.branches.map((branch, index) => { const edge = {}; var conditions = responseJson.branches[index].conditions; if (conditions === undefined || conditions === null) { conditions = []; } var label = ""; if (conditions.length === 1) { label = conditions.length + " condition"; } else if (conditions.length > 1) { label = conditions.length + " conditions"; } const sourceFound = insertedNodes.findIndex( (action) => action.data.id === branch.source_id ); if (sourceFound < 0) { return null; } const destinationFound = insertedNodes.findIndex( (action) => action.data.id === branch.destination_id ); if (destinationFound < 0) { return null; } edge.data = { id: branch.id, _id: branch.id, source: branch.source_id, target: branch.destination_id, label: label, conditions: conditions, hasErrors: branch.has_errors, decorator: false, source_workflow: responseJson.id, }; if (responseJson.public) { edge.data.is_valid = true edge.is_valid = true } return edge; }); edges = edges.filter((edge) => edge !== null); cy.removeListener("add"); cy.add(insertedNodes) cy.add(edges); if (nodefound === true) { const newId = uuidv4(); cy.add({ group: "edges", data: { id: newId, _id: newId, source: sourcenode.id, target: target_id, label: "Subflow", decorator: true, source_workflow: responseJson.id, }, }); } cy.fit(null, 100); cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); } else { if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.parentorg_workflow === "") { setOriginalWorkflow(responseJson) } setWorkflow(responseJson); setWorkflowDone(true); // Add error checks if (!responseJson.public) { if ( !responseJson.previously_saved || !responseJson.is_valid || responseJson.errors !== undefined || responseJson.errors !== null || responseJson.errors !== // what responseJson.errors.length > 0 ) { console.log("Setting configure Modal to open") } setConfigureWorkflowModalOpen(true) } } }) .catch((error) => { //toast(error.toString()); console.log("Get workflows error: ", error.toString()); }); }; const onUnselect = (event) => { const nodedata = event.target.data(); console.log("UNSELECT: ", nodedata); //if (nodedata.type === "ACTION") { // setLastSelected(nodedata) //} // // Wait for new node to possibly be selected //setTimeout(() => { const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { const item = typeIds[idkey] if (item.data.isButton === true) { //console.log("Reselect old node & return - or just return?") if (item.data.buttonType === "delete" && item.data.attachedTo === nodedata.id) { //console.log("delete of same node!") } return } } // Unselecting all //cy.elements().unselect() //if (nodedata.app_name === undefined && nodedata.source === undefined) { // return; //} //event.target.removeClass("selected"); // //// If button is clicked, select current node // Attempt at rewrite of name in other actions in following nodes. // Should probably be done in the onBlur for the textfield instead /* if (event.target.data().type === "ACTION") { const nodeaction = event.target.data() const curaction = workflow.actions.find(a => a.id === nodeaction.id) console.log("workflowaction: ", curaction) console.log("nodeaction: ", nodeaction) if (nodeaction.label !== curaction.label) { console.log("BEACH!") var params = [] const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_") for (var actionkey in workflow.actions) { if (workflow.actions[actionkey].id === curaction.id) { continue } for (var paramkey in workflow.actions[actionkey].parameters) { const param = workflow.actions[actionkey].parameters[paramkey] if (param.value === null || param.value === undefined || !param.value.includes("$")) { continue } const innername = param.value.toLowerCase().replace(" ", "_") if (innername.includes(fixedName)) { //workflow.actions[actionkey].parameters[paramkey].replace( //console.log("FOUND!: ", innername) } } } } } */ // FIXME - check if they have value before overriding like this for no reason. // Would save a lot of time (400~ ms -> 30ms) //console.log("ACTION: ", selectedAction) //console.log("APP: ", selectedApp) //setSubworkflow({}) ReactDOM.unstable_batchedUpdates(() => { setSelectedAction({}); setSelectedApp({}); setSelectedComment({}) setSelectedEdge({}) //setSelectedActionEnvironment({}) setTriggerAuthentication({}) setLocalFirstrequest(true) setSelectedTrigger({}); setSelectedTriggerIndex(-1) setUpdate(Math.random()) // Can be used for right side view setRightSideBarOpen(false); setScrollConfig({ top: 0, left: 0, selected: "", }); }) sendStreamRequest({ "item": "node", "type": "unselect", "id": workflow.id, }) //}, 150) }; const onEdgeSelect = (event) => { ReactDOM.unstable_batchedUpdates(() => { setRightSideBarOpen(true); setLastSaved(false); /* // Used to not be able to edit trigger-based branches. const triggercheck = workflow.triggers.find(trigger => trigger.id === event.target.data()["source"]) if (triggercheck === undefined) { */ if ( event.target.data("type") !== "COMMENT" && event.target.data().decorator ) { toast("This edge can't be edited."); } else { const destinationId = event.target.data("target"); const curaction = workflow.actions.find((a) => a.id === destinationId); //console.log("ACTION: ", curaction) if (curaction !== undefined && curaction !== null) { if ( curaction.app_name === "Shuffle Tools" && curaction.name === "router" ) { toast("Router action can't have incoming conditions"); event.target.unselect(); return; } } setSelectedEdgeIndex( workflow.branches.findIndex( (data) => data.id === event.target.data()["id"] ) ); setSelectedEdge(event.target.data()); } setSelectedAction({}); setSelectedTrigger({}); }) }; // Comparing locations between nodes and setting views var styledElements = []; var originalLocation = { x: 0, y: 0, }; const onCtxTap = (event) => { const nodedata = event.target.data(); console.log(nodedata); if (nodedata.type === "TRIGGER" && (nodedata.app_name === "Shuffle Workflow" || nodedata.app_name === "User Input")) { if (nodedata.parameters === null) { toast("Set a workflow first"); return; } if (nodedata.parameters === undefined) { return } const workflow_id = nodedata.parameters.find((param) => param.name === "workflow" || param.name === "subflow"); if (workflow.id === workflow_id.valu) { return; } cy.animation({ zoom: 0, center: { eles: event.target, }, }) .play() .promise() .then(() => { console.log("DONE: ", workflow_id); getWorkflow(workflow_id.value, nodedata); cy.fit(null, 50); }); } }; const onNodeDragStop = (event, selectedAction) => { const nodedata = event.target.data() if (nodedata.id === selectedAction.id) { //console.log("Same node, return") return } if (nodedata.finished === false) { return } const connected = event.target.connectedEdges().jsons() if (connected.length > 0 && connected !== undefined) { for (let connectkey in connected) { const edge = connected[connectkey] if (edge.data.decorator && edge.data.label === releaseToConnectLabel) { // Transform to normal edge const currentedge = cy.getElementById(edge.data.id) if (currentedge !== undefined && currentedge !== null) { currentedge.data("decorator", false) currentedge.data("label", "") } continue } const sourcenode = cy.getElementById(edge.data.source) const destinationnode = cy.getElementById(edge.data.target) if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { continue } const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) const currentedge = cy.getElementById(edge.data.id) if (currentedge !== undefined && currentedge !== null) { currentedge.style('control-point-distance', edgeCurve.distance) currentedge.style('control-point-weight', edgeCurve.weight) } } } if (styledElements.length === 1) { console.log( "Should reset location and autofill: ", styledElements, selectedAction ); if (originalLocation.x !== 0 || originalLocation.y !== 0) { const currentnode = cy.getElementById(nodedata.id); if (currentnode !== null && currentnode !== undefined) { currentnode.position("x", originalLocation.x); currentnode.position("y", originalLocation.y); } originalLocation = { x: 0, y: 0 }; } const curElement = document.getElementById(styledElements[0]); if (curElement !== null && curElement !== undefined) { curElement.style.border = curElement.style.original_border; var newValue = "$" + nodedata.label.toLowerCase().replaceAll(" ", "_"); if (nodedata.type === "TRIGGER") { if (nodedata.trigger_type === "WEBHOOK" || nodedata.trigger_type === "SCHEDULE" || nodedata.trigger_type === "EMAIL") { var newValue = "$exec" } } var paramname = ""; var idnumber = -1; if (curElement.id.startsWith("rightside_field_")) { // Find exact position to put the text const idsplit = curElement.id.split("_"); console.log(idsplit); if (idsplit.length === 3 && !isNaN(idsplit[2])) { selectedAction.parameters[idsplit[2]].value += newValue; paramname = selectedAction.parameters[idsplit[2]].name; idnumber = idsplit[2]; } } if (idnumber >= 0 && paramname.length > 0) { const exampledata = GetExampleResult(nodedata); const parsedname = paramname .toLowerCase() .trim() .replaceAll("_", " "); const foundresult = GetParamMatch(parsedname, exampledata, ""); if (foundresult.length > 0) { console.log("FOUND RESULT: ", paramname, foundresult); newValue = `${newValue}${foundresult}`; } selectedAction.parameters[idnumber].value = newValue; } curElement.value = newValue; } } if ( nodedata.app_name !== undefined && ((nodedata.app_name !== "Shuffle Tools" && nodedata.app_name !== "Testing" && nodedata.app_name !== "Shuffle Workflow" && nodedata.app_name !== "Integration Framework" && nodedata.app_name !== "Singul" && nodedata.app_name !== "User Input") || nodedata.isStartNode) ) { const allNodes = cy.nodes().jsons(); var found = false; for (let nodekey in allNodes) { const currentNode = allNodes[nodekey]; if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) { found = true break } } if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { if (!found) { console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) // Find how many executions it has var executions = 0 const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" const decoratorNode = { position: { x: event.target.position().x + 44, y: event.target.position().y + 44, }, locked: true, data: { isDescriptor: true, isValid: true, is_valid: true, isTrigger: true, label: `${matchingExecutions.length}`, attachedTo: nodedata.id, imageColor: color, hasExecutions: true, }, }; cy.add(decoratorNode) } } else { // Readding the icon after moving the node if (!found) { const iconInfo = GetIconInfo(nodedata); const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); const offset = nodedata.isStartNode ? 36 : 44; const decoratorNode = { position: { x: event.target.position().x + offset, y: event.target.position().y + offset, }, locked: true, data: { isDescriptor: true, isValid: true, is_valid: true, label: "", image: svgpin_Url, imageColor: iconInfo.iconBackgroundColor, attachedTo: nodedata.id, }, }; cy.add(decoratorNode).unselectify(); } else { //console.log("Node already exists - don't add descriptor node"); } } } originalLocation = { x: 0, y: 0, }; sendStreamRequest({ "item": "node", "type": "move", "id": nodedata.id, "location": { "x": event.target.position("x"), "y": event.target.position("y"), } }) }; const onNodeDrag = (event, selectedAction) => { const nodedata = event.target.data(); if (nodedata.finished === false) { console.log("NOT FINISHED - ADD EXAMPLE BRANCHES TO CLOSEST!!") return } if (nodedata.app_name !== undefined) { const allNodes = cy.nodes().jsons(); for (var nodekey in allNodes) { const currentNode = allNodes[nodekey]; if (currentNode.data.attachedTo === nodedata.id) { cy.getElementById(currentNode.data.id).remove(); } // Calculate location //currentNode.position.x > //if (nodedata.position.x > 0 && nodedata.position.y > 0) { // console.log("Positive both") //} //console.log(currentNode.position) //console.log(nodedata.position) } } else { //console.log("No appid? ", nodedata) } if (nodedata.buttonType === "edgehandler") { console.log("Enable edgehandler!") console.log("Find parent: ", nodedata.attachedTo) const parentNode = cy.getElementById(nodedata.attachedTo); if (parentNode !== null && parentNode !== undefined) { console.log("Start parentnode tracking!") //cy.edgehandles().start(parentNode) } } if (nodedata.id === selectedAction.id) { return; } if ((nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT" || nodedata.type === "ACTION") && !nodedata.isStartNode) { // Check if it already has any non-decorator branches attached to it const branches = cy.elements('edge').jsons() var branchFound = false var decoratorIds = [] for (var branchkey in branches) { if (branches[branchkey].data.source === nodedata.id || branches[branchkey].data.target === nodedata.id) { if (branches[branchkey].data.decorator === true) { // Add the source/destination if (branches[branchkey].data.source === nodedata.id) { decoratorIds.push(branches[branchkey].data.target) } else { decoratorIds.push(branches[branchkey].data.source) } continue } branchFound = true break } } if (!branchFound) { //console.log("Found action during drag. Checking closest nodes as it doesn't have a valid branch") var closestNode = null var minDistance = 300 const draggedNode = event.target const allnodes = cy.nodes().jsons() for (var nodekey in allnodes) { const node = allnodes[nodekey] if (node.data.id === nodedata.id) { continue } // Decorators if (node.data.attachedTo !== undefined) { continue } if (node.position === undefined || node.position === null || node.position.x === undefined || node.position.y === undefined) { continue } if (node.data.type !== "ACTION" && node.data.type !== "TRIGGER") { continue } const distance = Math.sqrt( Math.pow(draggedNode.position('x') - node.position.x, 2) + Math.pow(draggedNode.position('y') - node.position.y, 2) ) if (decoratorIds.includes(node.data.id)) { //console.log("Found existing decorator for: ", node.data.app_name, "Distance: ", distance) if (distance > 300) { // Remove the branch const edgeToRemove = cy.getElementById(branches[branchkey].data.id) if (edgeToRemove !== null && edgeToRemove !== undefined) { //console.log("Removing edge: ", edgeToRemove) edgeToRemove.remove() //decoratorIds.splice(decoratorIds.indexOf(node.data.id), 1) break } } } if (distance < minDistance) { minDistance = distance closestNode = node } } if (closestNode !== null && closestNode !== undefined) { //console.log("Closest node app: ", closestNode.data.app_name, "Distance: ", minDistance) /* if (decoratorIds.length > 0) { console.log("Decorators already exists. If within distance of 15 add to existing, otherwise remove old and add new: ", decoratorIds) for (var decoratorkey in decoratorIds) { const decoratorEdge = cy.getElementById(decoratorIds[decoratorkey]) if (decoratorEdge === null || decoratorEdge === undefined) { continue } const sourceNode = cy.getElementById(decoratorEdge.data.source) const targetNode = cy.getElementById(decoratorEdge.data.target) const distance = Math.sqrt( Math.pow(draggedNode.position('x') - sourceNode.position('x'), 2) + Math.pow(draggedNode.position('y') - sourceNode.position('y'), 2) ) // Check plus minus 15 in distance from mindistance if (distance > minDistance - 15 && distance < minDistance + 15) { console.log("Within distance of 15, add to existing edge") } else { console.log("Outside distance of 15, remove old edge and add new") } } } */ if (decoratorIds.length === 0) { //const edgeCurve = calculateEdgeCurve(draggedNode.position(), closestNode.position) //currentedge.style('control-point-distance', edgeCurve.distance) //currentedge.style('control-point-weight', edgeCurve.weight) const newId = uuidv4() cy.add({ group: "edges", data: { decorator: true, id: newId, _id: newId, source: closestNode.data.id, target: nodedata.id, label: releaseToConnectLabel, conditions: [], } }) } } } /* // FIXME: This is the start of a highlighter for the node // to better match it up with other elements // 1. Get current node's position in X/Y on the screen // 2. Draw a red line on the X and Y axis for positioning // Draw a red div line in the HTML const position = event.target.position() const redline = document.getElementById("redline") if (redline !== null && redline !== undefined) { redline.style.display = "block" redline.style.position = "absolute" redline.style.left = position.x + "px" redline.style.top = position.y + "px" redline.style.height = "10000px" redline.style.width = 1 console.log("REDLINE!") } */ } if (originalLocation.x === 0 && originalLocation.y === 0 && nodedata.position !== undefined) { originalLocation.x = nodedata.position.x; originalLocation.y = nodedata.position.y; } // Part of autocomplete. Styles elements in frontend to indicate // what and where we may input data for the user. const onMouseUpdate = (e) => { const x = e.pageX; const y = e.pageY; const elementMouseIsOver = document.elementFromPoint(x, y); if (elementMouseIsOver !== undefined && elementMouseIsOver !== null) { // Color for #f85a3e translated to rgb const newBorder = "3px solid rgb(248, 90, 62)"; if ( elementMouseIsOver.style.border !== newBorder && elementMouseIsOver.id.includes("rightside") ) { if (elementMouseIsOver.style.border !== undefined) { elementMouseIsOver.style.original_border = elementMouseIsOver.style.border; } else { elementMouseIsOver.style.original_border = ""; } elementMouseIsOver.style.border = newBorder; console.log("STYLED: ", styledElements); for (var styledElementsKey in styledElements) { const curElement = document.getElementById(styledElements[styledElementsKey]); if (curElement !== null && curElement !== undefined) { curElement.style.border = curElement.style.original_border; } } styledElements = []; styledElements.push(elementMouseIsOver.id); } else if ( elementMouseIsOver.id === "cytoscape_view" || elementMouseIsOver.id === "" ) { for (var index in styledElements) { const curElement = document.getElementById(styledElements[index]); if (curElement !== null && curElement !== undefined) { curElement.style.border = curElement.style.original_border; } } styledElements = []; } } // Ensure it only happens once document.removeEventListener("mousemove", onMouseUpdate, false); }; document.addEventListener("mousemove", onMouseUpdate, false); }; useBeforeunload(() => { if (!lastSaved) { return unloadText; } else { if (workflow.public === false) { //document.removeEventListener("mousemove", onMouseUpdate, true); document.removeEventListener("keydown", handleKeyDown, true); document.removeEventListener("paste", handlePaste, true); } } }); // Should get AI autocompletes const aiSubmit = (value, setResponseMsg, setSuggestionLoading, inputAction) => { if (setResponseMsg !== undefined) { setResponseMsg("") } if (value === undefined || value === "") { console.log("No value input!") return } if (setSuggestionLoading !== undefined) { setSuggestionLoading(true) } console.log("Submit conversation with value: ", value); // This is to find sample response and parse it as string var AppContext = [] var originalParams = [] if (inputAction !== undefined && inputAction !== null) { // Reload the data without copying inputAction = JSON.parse(JSON.stringify(inputAction)) originalParams = JSON.parse(JSON.stringify(inputAction.parameters)) const parents = getParents(inputAction) var actionlist = [] if (parents.length > 1) { for (let [key,keyval] in Object.entries(parents)) { const item = parents[key]; if (item.label === "Execution Argument") { continue; } var exampledata = item.example === undefined || item.example === null ? "" : item.example; // Find previous execution and their variables //exampledata === "" && if (workflowExecutions.length > 0) { // Look for the ID const found = false; for (let [key,keyval] in Object.entries(workflowExecutions)) { if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { continue; } var foundResult = workflowExecutions[key].results.find((result) => result.action.id === item.id); if (foundResult === undefined || foundResult === null) { continue; } if (foundResult.result !== undefined && foundResult.result !== null) { foundResult = foundResult.result } const valid = validateJson(foundResult, true) if (valid.valid) { if (valid.result.success === false) { //console.log("Skipping success false autocomplete") } else { exampledata = valid.result; break; } } else { exampledata = foundResult; } } } // 1. Take const itemlabelComplete = item.label === null || item.label === undefined ? "" : item.label.split(" ").join("_"); const actionvalue = { app_name: item.app_name, action_name: item.name, label: item.label, type: "action", id: item.id, name: item.label, autocomplete: itemlabelComplete, example: exampledata, }; actionlist.push(actionvalue); } } var fixedResults = [] for (var i = 0; i < actionlist.length; i++) { const item = actionlist[i]; const responseFix = SetJsonDotnotation(item.example, "") // Check if json const validated = validateJson(responseFix) var exampledata = responseFix; if (validated.valid) { exampledata = JSON.stringify(validated.result) } AppContext.push({ "app_name": item.app_name, "action_name": item.action_name, "label": item.label, "example": exampledata, //"example_response": exampledata, }) } var params = [] for (var paramkey in inputAction.parameters) { const param = inputAction.parameters[paramkey] if (param.configuration) { continue } // Mainly for booleans if (param.options !== undefined && param.options !== null && param.options.length > 0) { continue } params.push(param) } inputAction.parameters = params } var conversationData = { "query": value, "output_format": "action", "app_context": AppContext, "workflow_id": workflow.id, } if (inputAction !== undefined) { console.log("Add app context! This should them get parameters directly") conversationData.output_format = "action_parameters" conversationData.app_id = inputAction.app_id conversationData.app_name = inputAction.app_name conversationData.action_name = inputAction.name conversationData.parameters = inputAction.parameters } // Onprem not available yet (April 2023) // Should: Make OpenAI work for them with their own key //fetch(`${globalUrl}/api/v1/conversation`, { const url = `${globalUrl}/api/v1/conversation` fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(conversationData), credentials: "include", }) .then((response) => { setAiQueryModalOpen(false) setAutocompleting(false) if (setSuggestionLoading !== undefined) { setSuggestionLoading(false) } if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } else { toast("Completion finished. Please verify your action!") } return response.json(); }) .then((responseJson) => { console.log("Conversation response: ", responseJson) if (responseJson.success === false) { if (responseJson.reason !== undefined) { if (setResponseMsg !== undefined) { setResponseMsg(responseJson.reason) } } return } if (inputAction !== undefined) { console.log("In input action! Should check params if they match, and add suggestions") if (responseJson.parameters === undefined || responseJson.parameters.length === 0) { return } var changed = false for (let respParamKey in responseJson.parameters) { var respParam = responseJson.parameters[respParamKey] if (respParam.value === undefined || respParam.value === null || respParam.value === "" ) { continue } for (let paramkey in originalParams) { const actionParam = originalParams[paramkey] /* if (actionParam.value !== "" && actionParam.value !== actionParam.example) { console.log("Skipping: ", actionParam) continue } */ if (respParam.name !== actionParam.name) { continue } const codeeditor = document.getElementById("shuffle-codeeditor") if (codeeditor !== undefined && codeeditor !== null) { const editorInstance = window?.ace?.edit("shuffle-codeeditor") if (editorInstance === undefined || editorInstance === null) { toast.error("Failed to find code editor instance") return } else { editorInstance.setValue(respParam.value) originalParams[paramkey].autocompleted = true changed = true } } if (!changed) { console.log("Found match for param: ", respParam) changed = true originalParams[paramkey].autocompleted = true originalParams[paramkey].value = respParam.value } break } } if (changed === true) { inputAction.parameters = originalParams console.log("Setting action! Force update pls :)") setUpdate(Math.random()) setSelectedAction(inputAction) // Find it in cytoscape and update the action if (cy !== undefined && cy !== null) { const cyAction = cy.getElementById(inputAction.id) if (cyAction !== undefined && cyAction !== null) { cyAction.data("parameters", inputAction.parameters) } } } return } // Add action console.log("Suggestionbox location: ", suggestionBox) if (responseJson.app_name !== undefined && responseJson.app_name !== null) { // Always added to 0, 0 // Should use suggestionBox.position.x, suggestionBox.position.y var newitem = { "data": responseJson, "position": { "x": suggestionBox.node_position.x !== undefined ? suggestionBox.node_position.x : 0, "y": suggestionBox.node_position.y !== undefined ? suggestionBox.node_position.y + 100 : 0, }, "group": "nodes", } newitem.type = "ACTION" newitem.isStartNode = false newitem.data.id = uuidv4() newitem.data.type = "ACTION" newitem.data.isStartNode = false newitem.data.is_valid = true newitem.data.isValid = true cy.add({ group: newitem.group, data: newitem.data, position: newitem.position, }); // Add edge const newId = uuidv4() cy.add({ group: "edges", data: { id: newId, _id: newId, source: suggestionBox.attachedTo, target: newitem.data.id, } }) //label: "Generated", setSuggestionBox({ "position": { "top": 500, "left": 500, }, "open": false, "attachedTo": "", }); } }) .catch((error) => { setAiQueryModalOpen(false) setAutocompleting(false) if (setSuggestionLoading !== undefined) { setSuggestionLoading(false) } console.log("Conv response error: ", error); }); } // Nodeselectbatching: // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once // onNodeClick const onNodeSelect = (event, newAppAuth) => { // Forces all states to update at the same time, // Otherwise everything is SUPER slow // FIXME: Do absolutely NOT use JSON.stringify on the event.target.data() // This causes memory referencing to become a nightmare const data = event.target.data() if (data.app_name === "Shuffle Workflow") { if ((data.parameters !== undefined) && (data.parameters.length > 0)) { getWorkflowApps(data.parameters[0].value) } } if (data.buttonType == "ACTIONSUGGESTION") { const attachedToId = data.attachedTo const parentitemRaw = cy.getElementById(data.attachedTo) const parentitem = parentitemRaw.data() if (parentitem !== null && parentitem !== undefined) { setTimeout(() => { parentitemRaw.select() const allNodes = cy.nodes().jsons() for (var _key in allNodes) { const currentNode = allNodes[_key] if (currentNode.data.buttonType === "ACTIONSUGGESTION") { cy.getElementById(currentNode.data.id).remove() } } }, 100) const findaction = data.label console.log("CLICKED: ", findaction, apps.length) for (let appkey in apps) { const curapp = apps[appkey] if (curapp.name !== parentitem.app_name) { continue } if (curapp.actions === undefined || curapp.actions === null) { continue } for (let actionkey in curapp.actions) { const curaction = curapp.actions[actionkey] if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { if (curaction.category_label[0].toLowerCase() === findaction.toLowerCase()) { console.log("FOUND: ", curaction) // Update the action itself // Find the action index, and update: // - label // - description // - parameters // - name var foundindex = -1 for (let wfactionkey in workflow.actions) { const wfaction = workflow.actions[wfactionkey] if (wfaction.id === data.attachedTo) { foundindex = wfactionkey break } } console.log("Updating action: ", foundindex, findaction) if (foundindex >= 0) { workflow.actions[foundindex].label = findaction workflow.actions[foundindex].description = curaction.description workflow.actions[foundindex].parameters = curaction.parameters workflow.actions[foundindex].name = curaction.name setWorkflow(workflow) console.log(workflow) } break } } } break } return } } else if (data.isSuggestion === true) { console.log("Suggestion! Replace with a real action.") const attachedToId = data.attachedTo //event.target.data("attachedTo", "") const allNodes = cy.nodes().jsons(); for (var _key in allNodes) { const currentNode = allNodes[_key]; // console.log("CURRENT NODE: ", currentNode) if ((currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== data.id) { cy.getElementById(currentNode.data.id).remove(); } } // Add relevant fields for the action and connect it to the parent (attachedTo) //event.target.data("attachedTo", "") // Decides directionality // const isTarget = event.target.data("isTarget") const target = isTarget ? data.id : attachedToId const source = isTarget ? attachedToId : data.id const newId = uuidv4() // Add a new node const newAction = { ...data, } newAction.attachedTo = "" newAction.isSuggestion = false newAction.finished = true newAction.isButton = false newAction.private_id = "" newAction.type = "ACTION" if (newAction.app_name === "Shuffle Subflow") { newAction.type = "TRIGGER" newAction.trigger_type = "SUBFLOW" } else { newAction.decorator = false newAction.suggested = true } newAction.label = data.label newAction.id = uuidv4() // Find the app and add params? cy.add({ group: "nodes", data: newAction, position: { x: event.target.position().x, y: event.target.position().y, }, }) toast("Suggestion added!") setTimeout(() => { const newBranch = { source: source, target: newAction.id, _id: newId, id: newId, decorator: false, finished: true, } cy.add({ group: "edges", data: newBranch, }) setWorkflowRecommendations(undefined) // Find the new node we added from newAction.id const newActionNode = cy.getElementById(newAction.id) if (newActionNode !== undefined && newActionNode !== null) { newActionNode.select() } aiSubmit("Fill based on previous values", undefined, undefined, newAction) }, 1000) return } ReactDOM.unstable_batchedUpdates(() => { if (data.isButton) { if (data.buttonType === "suggestion") { if (cy === undefined) { console.log("Cy not defined yet") return } // Inject HTML at a fixed location? //const newHtml = "

Do you want to add this suggestion?

" // Find mouse cursor position on screen console.log("Suggestion html to be added at location: ", event) /* const position = { "top": cy.pan().y, "left": cy.pan().x, } */ const position = event.target.renderedPosition(); const container = cy.container(); const offset = { left: container.offsetLeft, top: container.offsetTop }; // Calculate the actual screen position for the box const screenPosition = { left: position.x + offset.left - 150, top: position.y + offset.top, }; // Log the position to the console console.log('Node screen position:', screenPosition); const newbox = { "position": screenPosition, "node_position": event.target.position(), "open": true, "attachedTo": data.attachedTo, } console.log("Rendered position: ", newbox.node_position) setSuggestionBox(newbox) // Unselect event.target.unselect(); } else if (data.buttonType === "delete") { const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined) { removeNode(data.attachedTo) //parentNode.remove() } return } else if (data.buttonType === "set_startnode" && data.type !== "TRIGGER") { //console.log("STARTNODE") //event.preventDefault() //event.stopPropagation() const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined) { var oldstartnode = cy.getElementById(workflow.start); if ( oldstartnode !== null && oldstartnode !== undefined && oldstartnode.length > 0 ) { try { oldstartnode[0].data("isStartNode", false); } catch (e) { console.log("Startnode error: ", e); } } workflow.start = parentNode.data("id"); setLastSaved(false); parentNode.data("isStartNode", true); } //event.target.unselect(); setRightSideBarOpen(true); return } else if (data.buttonType === "copy") { console.log("COPY!"); // 1. Find parent // 2. Find branches for parent // 3. Make a new node that's moved a little bit const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined && parentNode.data() !== undefined && parentNode.data() !== null) { var newNodeData = JSON.parse(JSON.stringify(parentNode.data())); newNodeData.id = uuidv4(); if (newNodeData.position !== undefined) { newNodeData.position = { x: newNodeData.position.x + 100, y: newNodeData.position.y + 100, }; } newNodeData.isStartNode = false; newNodeData.errors = []; newNodeData.is_valid = true; newNodeData.isValid = true; newNodeData.label = parentNode.data("label") + "_copy"; cy.add({ group: "nodes", data: newNodeData, position: newNodeData.position, }); // Readding the icon after moving the node if ( newNodeData.app_name !== "Testing" || newNodeData.app_name !== "Shuffle Workflow" ) { } else { const iconInfo = GetIconInfo(newNodeData); const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); const offset = newNodeData.isStartNode ? 36 : 44; const decoratorNode = { position: { x: newNodeData.position.x + offset, y: newNodeData.position.y + offset, }, locked: true, data: { isDescriptor: true, isValid: true, is_valid: true, label: "", image: svgpin_Url, imageColor: iconInfo.iconBackgroundColor, attachedTo: newNodeData.id, }, }; cy.add(decoratorNode).unselectify(); } workflow.actions.push(newNodeData); const sourcebranches = workflow.branches.filter((foundbranch) => foundbranch.source_id === parentNode.data("id")) const destinationbranches = workflow.branches.filter((foundbranch) => foundbranch.destination_id === parentNode.data("id")) for (var sourceBranchesKey in sourcebranches) { var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); newbranch.id = uuidv4() newbranch.source_id = newNodeData.id newbranch._id = newbranch.id newbranch.source = newbranch.source_id newbranch.target = newbranch.destination_id cy.add({ group: "edges", data: newbranch, }) } for (var destinationBranchesKey in destinationbranches) { var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) const sourcenode = cy.getElementById(newbranch.source_id) if (sourcenode !== null && sourcenode !== undefined) { const sourcedata = sourcenode.data() if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { continue } } newbranch.id = uuidv4() newbranch.destination_id = newNodeData.id newbranch._id = newbranch.id newbranch.source = newbranch.source_id newbranch.target = newbranch.destination_id cy.add({ group: "edges", data: newbranch, }) } //event.target.unselect(); return } } return; } else if (data.isDescriptor) { // Find parent event.target.unselect(); if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { const parentNode = cy.getElementById(data.attachedTo) if (parentNode !== null && parentNode !== undefined) { setTimeout(() => { parentNode.select() }, 100) } } //console.log("Can't select descriptor"); if (data.isTrigger) { console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") setExecutionModalOpen(true) } return; } if (data.type === undefined) { console.log("No type, automatically setting to action"); data.type = "ACTION" } if (data.type === "ACTION") { setSelectedComment({}) // FIXME: is this what is mapping it an actual action in the workflow? wtf? var curaction = workflow.actions.find((a) => a.id === data.id) if (!curaction || curaction === undefined) { if (data.id !== undefined && data.app_name !== undefined) { workflow.actions.push(data) setWorkflow(workflow) // FIXME: Is this necessary? //curaction = JSON.parse(JSON.stringify(data)) } else { if (workflow.public !== true) { toast("Action not found. Please remake it."); } event.target.remove() return } } // FIXME: This change may cause... something curaction = data //var newapps = JSON.parse(JSON.stringify(apps)) var newapps = apps if (apps === null || apps === undefined || apps.length === 0) { newapps = filteredApps } // Check ID first, then names etc // That way it always selects the right IF it exists var curapp = newapps.find((a) => a.id === curaction.app_id ) if (curapp === undefined || curapp === null) { console.log("Couldn't find app with that ID - checking with name & version") curapp = newapps.find((a) => a.name === curaction.app_name && (a.app_version === curaction.app_version || (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version))) ) } if (curapp === undefined || curapp === null) { curapp = integrationApps.find((a) => a.name === curaction.app_name && (a.app_version === curaction.app_version || (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version))) ) } if (curaction.template === true && curaction.name !== undefined) { //newapps. const parsedname = curaction.name.replaceAll(" ", "_").toLowerCase() console.log("FIND AN ACTION AMONG THE APPS THAT MATCHES NAME: ", parsedname) curaction.matching_actions = [] for (var newAppskey in newapps) { for (let actionsSubkey in newapps[newAppskey].actions) { const tmpaction = newapps[newAppskey].actions[actionsSubkey] if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { console.log("MATCH!: ", newapps[newAppskey]) curaction.matching_actions.push({ "app_name": newapps[newAppskey].name, "app_version": newapps[newAppskey].app_version, "app_id": newapps[newAppskey].id, "action": tmpaction, "large_image": newapps[newAppskey].large_image, "app_index": newAppskey, "action_index": actionsSubkey, }) } } } } if (!curapp || curapp === undefined) { // Check local storage has it const foundapps = localStorage.getItem("apps") if (foundapps !== null && foundapps !== undefined) { try { const parsedapps = JSON.parse(foundapps) if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { for (let appkey in parsedapps) { if (parsedapps[appkey].name === curaction.app_name) { curapp = parsedapps[appkey] break } } } } catch (e) { console.log("Problem with parsing apps from local storage", e) } } else { console.log("No apps found in local storage") } } /* if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { toast(`Side-loading app ${curapp.name} to get actions.`) } */ if (curapp !== undefined && curapp !== null && curapp.id !== undefined && curapp.id !== null && curapp.id.length > 0) { loadAppConfig(curapp.id, true) } if (!curapp || curapp === undefined) { const tmpapp = { name: curaction.app_name, app_name: curaction.app_name, app_version: curaction.app_version, id: curaction.app_id, actions: [curaction], } setSelectedApp(tmpapp) setSelectedAction(curaction) } else { curaction.app_id = curapp.id if (curapp.authentication === undefined || curapp.authentication === null) { setAuthenticationType({ type: "", }) curapp.authentication = { type: "", required: false, } } else { setAuthenticationType( curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { type: curapp.authentication.type, redirect_uri: curapp.authentication.redirect_uri, refresh_uri: curapp.authentication.refresh_uri, token_uri: curapp.authentication.token_uri, scope: curapp.authentication.scope, client_id: curapp.authentication.client_id, client_secret: curapp.authentication.client_secret, grant_type: curapp.authentication.grant_type, } : { type: "", } ) } const requiresAuth = curapp.authentication.required; //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) setRequiresAuthentication(requiresAuth); if (curapp.authentication.required) { //console.log("App requires auth.") // Setup auth here :) const authenticationOptions = []; var findAuthId = ""; if ( curaction.authentication_id !== null && curaction.authentication_id !== undefined && curaction.authentication_id.length > 0 ) { findAuthId = curaction.authentication_id; } const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); const curappName = curapp.name.toLowerCase() for (let tmpAuthKey in tmpAuth) { var item = tmpAuth[tmpAuthKey]; const newfields = {}; if (item.app.name.toLowerCase() !== curappName) { continue } // Makes list into key:value object for (let fieldkey in item.fields) { if (item.fields[fieldkey] === undefined) { console.log("Problem with filterkey in Node select", fieldkey) continue } const filterkey = item.fields[fieldkey]["key"] if (filterkey === null || filterkey === undefined) { console.log("Problem with filterkey 2. Null or undefined 3") continue } newfields[filterkey] = item.fields[fieldkey]["value"]; } item.fields = newfields; if (item.app.name.toLowerCase() === curappName) { authenticationOptions.push(item); if (item.id === findAuthId) { curaction.selectedAuthentication = item; } } } // Find with authenticationOption (authenticationOptions) has the highest .edited time. In this index, set the "last_modified" to true var latesttime = 0 var latestindex = -1 for (var i = 0; i < authenticationOptions.length; i++) { const authopt = authenticationOptions[i] if (authopt.edited > latesttime) { latesttime = authopt.edited latestindex = i } } if (latestindex !== -1) { authenticationOptions[latestindex].last_modified = true } curaction.authentication = authenticationOptions if ( curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "" ) { curaction.selectedAuthentication = {}; } } else { curaction.authentication = [] curaction.authentication_id = ""; curaction.selectedAuthentication = {}; } if ( curaction.parameters !== undefined && curaction.parameters !== null && curaction.parameters.length > 0 ) { for (var curActionParamKey in curaction.parameters) { if ( curaction.parameters[curActionParamKey].options !== undefined && curaction.parameters[curActionParamKey].options !== null && curaction.parameters[curActionParamKey].options.length > 0 && curaction.parameters[curActionParamKey].value === "" ) { curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; } } } else { console.log("Should check APP if it has the same params as ACTION") for (let actionKey in curapp.actions) { const tmpaction = curapp.actions[actionKey] if (tmpaction.name === curaction.name) { console.log("Found action - needs change?", tmpaction) if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) } break } } } // Fix authentication fields that may be missing in the UI if (curapp.authentication.required && !curapp?.authentication?.type?.includes("oauth")) { if (curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) { var actionChanged = false for (let paramKey in curapp.authentication.parameters) { var param = curapp.authentication.parameters[paramKey] if (curaction.parameters === undefined || curaction.parameters === null) { curaction.parameters = [] } var found = false for (let actionParamKey in curaction.parameters) { if (curaction.parameters[actionParamKey].name === param.name) { found = true break } } if (!found) { param.configuration = true curaction.parameters.push(param) actionChanged = true } } if (actionChanged && workflow.actions !== undefined && workflow.actions !== null) { // Find it in the workflow and set it for (let wfActionKey in workflow.actions) { if (workflow.actions[wfActionKey].id === curaction.id) { workflow.actions[wfActionKey] = curaction } } setWorkflow(workflow) } } } setSelectedApp(curapp) setSelectedAction(curaction) cy.removeListener("drag"); cy.removeListener("free"); cy.on("drag", "node", (e) => onNodeDrag(e, curaction)); cy.on("free", "node", (e) => onNodeDragStop(e, curaction)); } if (environments !== undefined && environments !== null && (typeof environments === "array" || typeof environments === "object")) { var parsedenv = environments //if (typeof environments === "object") { // parsedenv = [environments] //} const envs = parsedenv.find((a) => a.Name === curaction.environment); var env = environments[defaultEnvironmentIndex] if (envs !== undefined && envs !== null) { env = envs } setSelectedActionEnvironment(env); } } else if (data.type === "TRIGGER") { setSelectedComment({}) if (workflow.triggers === null) { workflow.triggers = [] } var trigger_index = workflow.triggers.findIndex( (a) => a.id === data.id ) if (trigger_index === -1) { workflow.triggers.push(data) trigger_index = workflow.triggers.length - 1 setWorkflow(workflow) } if (data.app_name === "Shuffle Workflow" || data.app_name === "User Input") { // Check if public workflow if (workflow.public === true) { setWorkflows([workflow]) } else { getAvailableWorkflows(trigger_index); getSettings(); } } else if (data.app_name === "Webhook") { if (workflow.triggers[trigger_index].parameters !== undefined && workflow.triggers[trigger_index].parameters !== null && workflow.triggers[trigger_index].parameters.length > 0) { workflow.triggers[trigger_index].parameters[0] = { name: "url", value: referenceUrl + "webhook_" + workflow.triggers[trigger_index].id, }; if (workflow.triggers[trigger_index].parameters.length < 5) { console.log("Adding to webhook params!") workflow.triggers[trigger_index].parameters.push({ name: "await_response", value: "v1," }) } } } else if (data.app_name === "Pipeline") { // Check if environment is set if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { for (var envKey in environments) { if (environments[envKey].archived === true) { continue } if (environments[envKey].Name.toLowerCase() === "cloud") { continue } workflow.triggers[trigger_index].environment = environments[envKey].Name data.environment = environments[envKey].Name //setSelectedTrigger(data) break } } } setTimeout(() => { if (trigger_index !== -1) { const trigger = workflow.triggers[trigger_index] if (trigger !== undefined && trigger !== null) { // Autofixer if (trigger.trigger_type === "USERINPUT") { const relevantparams = [ "alertinfo", "options", "type", "email", "sms", "subflow", ] var foundparams = 0 for (var paramkey in trigger.parameters) { if (relevantparams.includes(trigger.parameters[paramkey].name)) { foundparams++ } } if (foundparams < 6) { trigger.parameters = [{ name: "alertinfo", value: "Do you want to continue the workflow? Start parameters: $exec", },{ name: "options", value: "boolean", }, { name: "type", value: "subflow", }, { name: "email", value: "test@test.com", }, { name: "sms", value: "0000000", }, { name: "subflow", value: "", }] workflow.triggers[trigger_index].parameters = trigger.parameters } } } } if (allTriggers !== undefined && allTriggers !== null) { // Just checking all three. Could just make a new list, but meh if (allTriggers.pipelines !== undefined && allTriggers.pipelines !== null) { for (var pipelineKey in allTriggers.pipelines) { if (allTriggers.pipelines[pipelineKey].id === data.id) { data.status = allTriggers.pipelines[pipelineKey].status } } } if (allTriggers.webhooks !== undefined && allTriggers.webhooks !== null) { for (var webhookKey in allTriggers.webhooks) { if (allTriggers.webhooks[webhookKey].id === data.id) { data.status = allTriggers.webhooks[webhookKey].status } } } if (allTriggers.schedules !== undefined && allTriggers.schedules !== null) { for (var scheduleKey in allTriggers.schedules) { if (allTriggers.schedules[scheduleKey].id === data.id) { data.status = allTriggers.schedules[scheduleKey].status } } } } setSelectedTriggerIndex(trigger_index) setSelectedTrigger(data) //setSelectedActionEnvironment(data.env) }, 25) } else if (data.type === "COMMENT") { setSelectedComment(data); } else { toast("Can't handle node type " + data.type); return; } setRightSideBarOpen(true); //setLastSaved(false); setScrollConfig({ top: 0, left: 0, selected: "", }); setSuggestionBox({ "position": { "top": 500, "left": 500, }, "open": false, "attachedTo": "", }); sendStreamRequest({ "item": "node", "type": "select", "id": data.id, "location": { "x": event.target.position("x"), "y": event.target.position("y"), } }) }) } const activateApp = (appid, refresh) => { fetch(`${globalUrl}/api/v1/apps/${appid}/activate`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Failed to activate") } return response.json() }) .then((responseJson) => { if (responseJson.success === false) { toast("Failed to auto-activate the app. Go to /apps and activate it.") } else { if (refresh === true) { setHighlightedApp(appid) //toast("App activated for your organisation! Refresh the page to use the app.") getApps() } } }) .catch(error => { //toast(error.toString()) console.log("Activate app error: ", error.toString()) }); } const GetExampleResult = (item) => { var exampledata = item.example === undefined ? "" : item.example; if (workflowExecutions.length > 0) { // Look for the ID for (let execkey in workflowExecutions) { if ( workflowExecutions[execkey].results === undefined || workflowExecutions[execkey].results === null ) { continue; } var foundResult = { result: "" }; if (item.id === "exec") { if ( workflowExecutions[execkey].execution_argument !== undefined && workflowExecutions[execkey].execution_argument !== null && workflowExecutions[execkey].execution_argument.length > 0 ) { foundResult.result = workflowExecutions[execkey].execution_argument; } else { continue; } } else { foundResult = workflowExecutions[execkey].results.find( (result) => result.action.id === item.id ); if (foundResult === undefined) { continue; } } foundResult.result = foundResult.result.trim(); foundResult.result = foundResult.result.split(" None").join(' "None"'); foundResult.result = foundResult.result.split(" False").join(" false"); foundResult.result = foundResult.result.split(" True").join(" true"); var jsonvalid = true; try { if ( !foundResult.result.includes("{") && !foundResult.result.includes("[") ) { jsonvalid = false; } } catch (e) { try { foundResult.result = foundResult.result.split("'").join('"'); if ( !foundResult.result.includes("{") && !foundResult.result.includes("[") ) { jsonvalid = false; } } catch (e) { jsonvalid = false; } } // Finds the FIRST json only if (jsonvalid) { try { exampledata = JSON.parse(foundResult.result) } catch (e) { console.log("Result: ", exampledata) } break; } } } return exampledata; }; const GetParamMatch = (paramname, exampledata, basekey) => { if (typeof exampledata !== "object") { return ""; } if (exampledata === null) { return ""; } //console.log("NOT REPLACING ON PURPOSE!!") //return "" // Basically just a stupid if-else :) const synonyms = { id: [ "id", "ref", "sourceref", "reference", "sourcereference", "alert id", "case id", "incident id", "service id", "sid", "uid", "uuid", "team id", "message id", "message_id", ], title: ["title", "name", "message"], description: ["description", "explanation", "story", "details"], email: ["mail", "email", "sender", "receiver", "recipient"], data: [ "data", "ip", "domain", "url", "hash", "md5", "sha2", "sha256", "value", "item", ], tags: ["tags", "taxonomies"], }; // 1. Find the right synonym // 2. Replace with an autocomplete if it exists var selectedsynonyms = [paramname]; for (const [key, value] of Object.entries(synonyms)) { if (key === paramname || value.includes(paramname)) { if (!value.includes(key)) { value.push(key.toLowerCase()); } selectedsynonyms = value; break; } } var toreturn = ""; for (const [key, value] of Object.entries(exampledata)) { // Check if loop or JSON if (typeof value === "object") { if (Array.isArray(value)) { var selectedkey = ""; if (isNaN(key)) { selectedkey = `.${key}`; } for (let [subitem,subitemval] in Object.entries(value)) { toreturn = GetParamMatch( paramname, value[subitem], `${basekey}${selectedkey}.#` ); if (toreturn.length > 0) { break; } } if (toreturn.length > 0) { break; } } else { var selectedkey = ""; if (isNaN(key)) { selectedkey = `.${key}`; } toreturn = GetParamMatch( paramname, value, `${basekey}${selectedkey}` ); if (toreturn.length > 0) { break; } } } else { if (selectedsynonyms.includes(key.toLowerCase())) { toreturn = `${basekey}.${key}` break } } } return toreturn; }; // Takes an action as input, then runs through and updates the relevant fields // based on previous actions' // Uses lots of synonyms // autocomplete const RunAutocompleter = (dstdata) => { // **PS: The right action should already be set here** // 1. Check execution argument // 2. Check parents in order var exampledata = GetExampleResult({ id: "exec", name: "exec" }); var parentlabel = "exec"; for (var dstdataParamKey in dstdata.parameters) { const param = dstdata.parameters[dstdataParamKey]; // Skip authentication params if (param.configuration) { continue } if (param.options !== undefined && param.options !== null && param.options.length > 0) { continue } const paramname = param.name.toLowerCase().trim().replaceAll("_", " "); const foundresult = GetParamMatch(paramname, exampledata, ""); if (foundresult.length > 0) { if (dstdata.parameters[dstdataParamKey].value.length === 0) { dstdata.parameters[dstdataParamKey].value = `$${parentlabel}${foundresult}`; dstdata.parameters[dstdataParamKey].autocompleted = true } } } var parents = getParents(dstdata); if (parents.length > 1) { for (let parentkey in parents) { const item = parents[parentkey]; if (item.label === "Execution Argument") { continue; } parentlabel = item.label === undefined ? "" : item.label.toLowerCase().trim().replaceAll(" ", "_"); exampledata = GetExampleResult(item); if (dstdata.parameters !== undefined && dstdata.parameters !== null) { for (let [paramkey,paramkeyval] in Object.entries(dstdata.parameters)) { const param = dstdata.parameters[paramkey]; // Skip authentication params if (param.configuration) { continue } if (param.options !== undefined && param.options !== null && param.options.length > 0) { continue } const paramname = param.name .toLowerCase() .trim() .replaceAll("_", " "); const foundresult = GetParamMatch(paramname, exampledata, ""); if (foundresult.length > 0) { if (dstdata.parameters[paramkey].value.length === 0) { dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; dstdata.parameters[paramkey].autocompleted = true } else { //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; } } } } } } return dstdata; }; // Checks for errors in edges when they're added const onEdgeAdded = (event) => { setLastSaved(false); const edge = event.target.data(); if (edge.source === undefined && edge.target === undefined) { //console.log("Edge added without source or target") return } if (edge.readded === true) { console.log("Readded edge - stopping") event.target.data("readded", false) return } const sourcenode = cy.getElementById(edge.source) const destinationnode = cy.getElementById(edge.target) if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) } else { if (sourcenode.data("name") === "switch") { event.target.remove() return } console.log("Edge added: Is it a trigger? If so, check if it already has a branch and remove it: ", sourcenode.data()) if (sourcenode.data("type") === "TRIGGER") { if (sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { setTimeout(() => { const alledges = cy.edges().jsons() var targetedge = alledges.findIndex( (data) => data.data.source === edge.source && data.data.id !== edge.id ) if (targetedge !== -1) { event.target.remove() //console.log("Found branch already!") toast.error("Triggers can have exactly one target node") return // name: "Shuffle Workflow", // name: "User Input", } else { console.log("Node doesn't already have one") } }, 50) } } const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) const currentedge = cy.getElementById(edge.id) if (currentedge !== undefined && currentedge !== null) { currentedge.style('control-point-distance', edgeCurve.distance) currentedge.style('control-point-weight', edgeCurve.weight) } } var targetnode = workflow.triggers.findIndex( (data) => data.id === edge.target ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { console.log("User Input or Shuffle Workflow") } else { toast("Can't have triggers as target of branch") event.target.remove() } } const eventTarget = event.target.target() console.log("BUTTON ADDED! Find parent from: ", eventTarget) if (eventTarget.data("isButton") === true) { const parentNode = cy.getElementById(eventTarget.data("attachedTo")) event.target.remove() console.log("Setting it to parentnode: ", parentNode.data()) if (parentNode !== undefined && parentNode !== null) { //event.target.data("target", eventTarget.data("attachedTo")) const newEdgeUuid = uuidv4() const newcybranch = { source: event.target.data("source"), target: eventTarget.data("attachedTo"), _id: newEdgeUuid, id: newEdgeUuid, hasErrors: event.target.data("hasErrors"), }; const edgeToBeAdded = { group: "edges", data: newcybranch, } cy.add(edgeToBeAdded); } } if (eventTarget.data("isDescriptor") === true || eventTarget.data("type") === "COMMENT") { console.log("Removing because of descriptor or comment") event.target.remove() return } targetnode = -1; // Check if: // dest == source && source == dest // dest == dest && source == source // backend: check all children? to stop recursion // var found = false; for (let branchkey in workflow.branches) { if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) { toast("A branch in the opposite direction already exists") event.target.remove() found = true break } if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { console.log("That branch already exists: ", workflow.branches[branchkey]) const foundbranch = cy.getElementById(workflow.branches[branchkey].id) if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { console.log("Removing branch: ", foundbranch.data()) event.target.remove() found = true break } else { //console.log("Old branch didn't exist afterall. Remove.") } } if (edge.target === workflow.start) { targetnode = workflow.triggers.findIndex( (data) => data.id === edge.source ); if (targetnode === -1) { if (targetnode.type !== "TRIGGER") { toast("Can't make arrow to starting node"); event.target.remove(); break; } found = true; } } if (edge.source === workflow.branches[branchkey].source_id) { // FIXME: Verify multi-target for triggers // 1. Check if destination exists // 2. Check if source is a trigger // targetnode = workflow.triggers.findIndex(data => data.id === edge.source) // console.log("Destination: ", edge.target) // console.log("CHECK SOURCE IF ITS A TRIGGER: ", targetnode) // if (targetnode !== -1) { // toast("Triggers can only target one target (startnode)") // event.target.remove() // found = true // break // } } else { //console.log("INSIDE LAST CHECK: ", edge) // Find the targetnode and check if its a trigger // FIXME - do this for both actions and other types? /* targetnode = workflow.triggers.findIndex(data => data.id === edge.target) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") { } else { toast("Can't have triggers as target of branch") event.target.remove() found = true break } } */ } } // 1. Guess what the next node's action should be // 2. Get result from previous nodes (if any) // 3. TRY to automatically map them in based on synonyms const newsource = cy.getElementById(edge.source); const newdst = cy.getElementById(edge.target); if ( newsource !== undefined && newsource !== null && newdst !== undefined && newdst !== null ) { const dstdata = RunAutocompleter(newdst.data()); //console.log("DST Autocompleter: ", dstdata); } var newbranch = { source_id: edge.source, destination_id: edge.target, id_: edge.id, id: edge.id, hasErrors: false, decorator: false, }; if (!found) { newbranch["hasErrors"] = false; workflow.branches.push(newbranch); setWorkflow(workflow); } history.push({ type: "edge", action: "added", data: edge, }); setHistory(history); setHistoryIndex(history.length); }; const onNodeAdded = (event) => { const node = event.target; const nodedata = JSON.parse(JSON.stringify(event.target.data())) if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined)) { return } // DONT MOVE THIS LINE RIGHT HERE v setLastSaved(false) if (node.isNode() && cy.nodes().size() === 1) { workflow.start = node.data("id") nodedata.isStartNode = true } else { if (workflow.actions === null) { console.log("Returning because node has no value") return } // Remove bad startnode for (let actionkey in workflow.actions) { const action = workflow.actions[actionkey]; if (action.isStartNode && workflow.start !== action.id) { action.isStartNode = false } } } if (nodedata.decorator !== true && nodedata.attachedTo === undefined) { var newdata = JSON.parse(JSON.stringify(nodedata)) newdata.large_image = "" sendStreamRequest({ "item": "node", "type": "add", "id": nodedata.id, "data": nodedata, "x": node.position("x"), "y": node.position("y"), }) } if (nodedata.type === "ACTION") { // Should get recommendations to load in for all nodesma if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) { const newEdgeUuid = uuidv4(); const newcybranch = { source: workflow.start, target: nodedata.id, _id: newEdgeUuid, id: newEdgeUuid, hasErrors: false, }; const edgeToBeAdded = { group: "edges", data: newcybranch, }; console.log("SHOULD STITCH WITH STARTNODE"); cy.add(edgeToBeAdded); } if (nodedata.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(nodedata); const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); nodedata.large_image = svgpin_Url; nodedata.fillGradient = iconInfo.fillGradient; nodedata.fillstyle = "solid"; if ( nodedata.fillGradient !== undefined && nodedata.fillGradient !== null && nodedata.fillGradient.length > 0 ) { nodedata.fillstyle = "linear-gradient"; } else { nodedata.iconBackground = iconInfo.iconBackgroundColor; } } if (nodedata.parameters !== undefined && nodedata.parameters !== null && !nodedata?.label?.endsWith("_copy")) { var newparameters = []; for (let [subkey,subkeyval] in Object.entries(nodedata.parameters)) { var newparam = JSON.parse(JSON.stringify(nodedata.parameters[subkey])) newparam.id = uuidv4() if (newparam.value === undefined || newparam.value === null) { newparam.value = "" } else { newparam.value = newparam.value } newparameters.push(newparam); } nodedata.parameters = newparameters } if (workflow.actions === undefined || workflow.actions === null) { workflow.actions = [nodedata]; } else { workflow.actions.push(nodedata); } // 1. Check how many actions there are. If less than three, send a toast notification with suggested workflows //if (workflow.actions.length < 3) { // toast("Recommendations to show??") //} setWorkflow(workflow); fetchRecommendations(workflow) } else if (nodedata.type === "TRIGGER") { if (nodedata.is_valid === false) { toast("This trigger is not available to you"); node.remove(); return; } if (workflow.triggers === undefined) { workflow.triggers = [nodedata]; } else { workflow.triggers.push(nodedata); } const newEdgeUuid = uuidv4(); const newcybranch = { source: nodedata.id, target: workflow.start, source_id: nodedata.id, destination_id: workflow.start, _id: newEdgeUuid, id: newEdgeUuid, hasErrors: false, decorator: false, }; const edgeToBeAdded = { group: "edges", data: newcybranch, }; if (edgeToBeAdded.data.source !== edgeToBeAdded.data.target && edgeToBeAdded.data.source !== undefined && edgeToBeAdded.data.target !== undefined) { if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { console.log("NAME: ", nodedata.name) if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { console.log("Edge handle: ", edgeToBeAdded) cy.add(edgeToBeAdded); } } } setWorkflow(workflow); } if (nodedata.app_name !== undefined) { history.push({ type: "node", action: "added", data: nodedata, }); setHistory(history); setHistoryIndex(history.length); } }; const onEdgeRemoved = (event) => { setLastSaved(false); const edge = event.target; if (edge.data("decorator") === true) { return; } // Check if the source is trigger and can start //console.log("Removed: ", edge.data()) const allNodes = cy.nodes().jsons() for (let nodekey in allNodes) { const curnode = allNodes[nodekey] if (curnode.data.type !== "TRIGGER") { continue } if (curnode.data.id === edge.data("source")) { console.log("Found matching trigger source: ", curnode) if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { // If it's started, READD the edge if (curnode.data.status === "running") { //console.log("Edge is running - readd it: ", edge.data()) // Just making sure it's not running infinitely var newdata = edge.data() newdata.readded = true try { cy.add({ group: "edges", data: newdata, }) //toast.error("You must STOP the trigger before deleting its branches") console.log("You must STOP the trigger before deleting its branches") } catch (e) { console.log("Failed re-adding edge: ", e) } } //status: "uninitialized", } } } workflow.branches = workflow.branches.filter( (a) => a.id !== edge.data().id ); setWorkflow(workflow); event.target.remove(); // trigger as source check const indexcheck = workflow.triggers.findIndex( (data) => edge.data("source") === data.id ); if (indexcheck !== -1) { console.log("Shouldnt remove edge from trigger? "); } if (edge.data().source !== undefined) { history.push({ type: "edge", action: "removed", data: edge.data(), }); setHistory(history); setHistoryIndex(history.length); } }; const onNodeRemoved = (event) => { const node = event.target; const data = node.data(); // FIXME: This is still a bit buggy if (data.decorator !== true && data.attachedTo === undefined) { sendStreamRequest({ "item": "node", "type": "remove", "id": data.id, }) } if (data.finished === false) { return } workflow.actions = workflow.actions.filter((a) => a.id !== data.id); workflow.triggers = workflow.triggers.filter((a) => a.id !== data.id); if (workflow.start === data.id && workflow.actions.length > 0) { // FIXME - should check branches connected to startnode, as picking random // is just confusing if (workflow.actions[0].id !== data.id) { const ele = cy.getElementById(workflow.actions[0].id); if (ele !== undefined && ele !== null) { ele.data("isStartNode", true); workflow.start = ele.id(); } } else { if (workflow.actions.length > 1) { const ele = cy.getElementById(workflow.actions[1].id); if (ele !== undefined && ele !== null) { ele.data("isStartNode", true); workflow.start = ele.id(); } } } } if (data.app_name !== undefined) { const allNodes = cy.nodes().jsons(); for (let allNodesKey in allNodes) { const currentNode = allNodes[allNodesKey]; if (currentNode.data.attachedTo === data.id) { cy.getElementById(currentNode.data.id).remove(); } } history.push({ type: "node", action: "removed", data: data, }) //console.log("REMOVED: ", data) setHistory(history); setHistoryIndex(history.length); } setWorkflow(workflow); } //var previouskey = 0 const handleKeyDown = (event) => { switch (event.keyCode) { case 27: if (configureWorkflowModalOpen === true) { setConfigureWorkflowModalOpen(false); } break; case 46: console.log("DELETE"); break; case 38: //console.log("UP"); break; case 37: //console.log("LEFT"); break; case 40: //console.log("DOWN"); break; case 39: //console.log("RIGHT"); break; case 90: if (event.ctrlKey) { console.log("CTRL+Z"); } break; case 67: if (event.ctrlKey && !event.shiftKey) { if ( event.path !== undefined && event.path !== null && event.path.length > 0 ) { if (event.path[0].localName !== "body") { console.log("Skipping because body is not targeted"); return; } } if ( event.target !== undefined && event.target !== null ) { if (event.target.localName !== "body") { console.log("Skipping because body is not targeted") return; } } console.log("CTRL+C"); if (cy !== undefined) { var cydata = cy.$(":selected").jsons(); if (cydata !== undefined && cydata !== null && cydata.length > 0) { console.log(cydata); const elementName = "copy_element_shuffle"; var copyText = document.getElementById(elementName); if (copyText !== null && copyText !== undefined) { const clipboard = navigator.clipboard; if (clipboard === undefined) { toast("Can only copy over HTTPS (port 3443)"); return; } navigator.clipboard.writeText(JSON.stringify(cydata)); copyText.select(); copyText.setSelectionRange(0, 99999); /* For mobile devices */ document.execCommand("copy"); toast(`Copied ${cydata.length} element(s)`); } } } } break; case 86: if (event.ctrlKey) { //console.log("CTRL+V") // The below parts are handled in the function handlePaste() /* const clipboard = navigator.clipboard if (clipboard === undefined || window === undefined || window === null) { toast("Can only use cliboard over HTTPS (port 3443)") return } console.log("CLIPBOARD: ", window.clipboardData) const pastedData = window.clipboardData.getData('Text'); console.log("PASTED: ", pastedData) //var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) var jsonvalid = true var parsedjson = [] */ } break; case 88: if (event.ctrlKey) { console.log("CTRL+X"); } break; case 83: break; case 70: break; case 65: // As a poweruser myself, I found myself hitting this a few // too many times to just edit text. Need a better bind, which does NOT work while inside a field break; default: break; } //previouskey = event.keyCode }; const handlePaste = (event) => { //console.log("EV: ", event) if ( event.path !== undefined && event.path !== null && event.path.length > 0 ) { //console.log("PATH: ", event.path[0]) if (event.path[0].localName !== "body") { //console.log("Skipping because body is not targeted") return; } } //console.log("PATH2: ", event.target) if ( event.target !== undefined && event.target !== null ) { if (event.target.localName !== "body") { //console.log("Skipping because body is not targeted") return; } } event.preventDefault(); const clipboard = (event.originalEvent || event).clipboardData.getData( "text/plain" ); //console.log("Text: ", clipboard) //window.document.execCommand('insertText', false, text); // try { var parsedjson = JSON.parse(clipboard); // Check if array if (!Array.isArray(parsedjson)) { console.log("Not array! Adding to array.") parsedjson = [parsedjson] } for (let jsonkey in parsedjson) { var item = parsedjson[jsonkey]; console.log("Adding: ", item); if (item.data === undefined || item.data === null) { console.log("Appending from here") const newitem = { "data": item, "position": { "x": 0, "y": 0 }, "group": "nodes", } item = newitem item.type = "ACTION" item.isStartNode = false item.data.type = "ACTION" item.data.isStartNode = false } item.data.id = uuidv4() cy.add({ group: item.group, data: item.data, position: item.position, }); } } catch (e) { console.log("Error pasting: ", e); //toast("Failed parsing clipboard: ", e) } }; const registerKeys = () => { document.addEventListener("keydown", handleKeyDown); document.addEventListener("paste", handlePaste); }; const getEnvironments = (orgId) => { var headers = { "Content-Type": "application/json", "Accept": "application/json", } if (orgId !== undefined && orgId !== null && orgId.length > 0) { headers["Org-Id"] = orgId } fetch(globalUrl + "/api/v1/getenvironments", { method: "GET", headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for envs :O!"); if (isCloud) { setEnvironments([{ Name: "Cloud", Type: "cloud" }]); } else { setEnvironments([{ Name: "Onprem", Type: "onprem" }]); } return; } return response.json(); }) .then((responseJson) => { var found = false var showEnvCnt = 0 for (let jsonkey in responseJson) { if (responseJson[jsonkey].default && !found) { setDefaultEnvironmentIndex(jsonkey) found = true } if (responseJson[jsonkey].archived === false) { showEnvCnt += 1 } } if (showEnvCnt > 1) { setShowEnvironment(true) } if (!found) { for (let jsonkey in responseJson) { if (!responseJson[jsonkey].archived) { setDefaultEnvironmentIndex(jsonkey) break; } } } if (isCloud) { if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { setEnvironments(responseJson) } else { setEnvironments([{ Name: "Cloud", Type: "cloud" }]) } } else { setEnvironments(responseJson) } /* setTimeout(() => { console.log("ACTIONS: ", workflow.actions) if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { for (var actionkey in workflow.actions) { if (workflow.actions[actionkey].environment !== undefined && workflow.actions[actionkey].environment !== null && workflow.actions[actionkey].environment.length > 0) { const env = environments.findIndex((data) => data.Name === workflow.actions[actionkey].environment) if (env !== -1) { setSelectedActionEnvironment(environments[env]) } } } } }, 2500) */ }) .catch((error) => { //toast(error.toString()); console.log("Get environments error: ", error.toString()); }); }; if ( !firstrequest && graphSetup && established && props.match.params.key !== workflow.id && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ) { // Check if if (distributedFromParent === "" && suborgWorkflows === []) { toast.info("Redirecting as the workflow ID does not match the URL") setTimeout(() => { window.location.pathname = "/workflows/" + props.match.params.key; }, 2500) } } const animationDuration = 150; const onNodeHoverOut = (event) => { const nodedata = event.target.data(); const cytoscapeElement = document.getElementById("cytoscape_view") if (cytoscapeElement !== undefined && cytoscapeElement !== null) { cytoscapeElement.style.cursor = "default" } if (nodedata.finished === false) { // Should just be 1, so this should be fast enough :3 const incomingEdges = event.target.incomers("edge").jsons() if (incomingEdges !== undefined && incomingEdges !== null) { for (var i = 0; i < incomingEdges.length; i++) { // Find the actual edge const edge = cy.getElementById(incomingEdges[i].data.id) if (edge === undefined || edge === null) { console.log("edge is null or undefined") continue } // Set the edge to be dashed edge.style("target-arrow-color", "#555555") edge.style("line-style", "dashed") edge.style("line-gradient-stop-colors", ["#555555", "#555555"]) } } return } if (nodedata.name === "switch") { return } // console.log("nodedata", nodedata); // console.log("nodedata.app_name: ", nodedata.app_name); if (nodedata.app_name !== undefined) { const allNodes = cy.nodes().jsons(); // console.log("allNodes: ", allNodes) for (var nodekey in allNodes) { const currentNode = allNodes[nodekey]; // console.log("Current node: ", currentNode); if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { if (currentNode.data.buttonType === "condition-drag") { continue } cy.getElementById(currentNode.data.id).remove(); } } } // Skipping node editing if it's the selected one if (cy !== undefined) { const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { const item = typeIds[idkey] if (item.data.id === nodedata.id) { return } } } //if (nodedata.id === selectedAction.id || nodedata.id === selectedTrigger.id) { // return //} var parsedStyle = { "border-width": "1px", "font-size": "18px", //"cursor": "default", } if ((nodedata.app_name === "Testing" || nodedata.app_name === "Shuffle Tools") && !nodedata.isStartNode) { parsedStyle = { "border-width": "1px", "font-size": "0px", }; } event.target.animate( { style: parsedStyle, }, { duration: animationDuration, } ); const outgoingEdges = event.target.outgoers("edge"); const incomingEdges = event.target.incomers("edge"); if (outgoingEdges.length > 0) { outgoingEdges.removeClass("hover-highlight"); } if (incomingEdges.length > 0) { outgoingEdges.removeClass("hover-highlight"); } }; const buttonColor = "rgba(255,255,255,0.9)"; const buttonBackgroundColor = "#1f2023"; const addStartnodeButton = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (parentNode.data("isStartNode")) { return; } const px = parentNode.position("x") - 65; const py = parentNode.position("y") - 45; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); const iconInfo = { icon: "M9.4 2H15V12H8L7.6 10H2V17H0V0H9L9.4 2ZM9 10H11V8H13V6H11V4H9V6L8 4V2H6V4H4V2H2V4H4V6H2V8H4V6H6V8H8V6L9 8V10ZM6 6V4H8V6H6ZM9 6H11V8H9V6Z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); cy.add({ group: "nodes", data: { weight: 30, id: circleId, name: "TEEEXT", isButton: true, buttonType: "set_startnode", attachedTo: event.target.data("id"), icon: svgpin_Url, iconBackground: iconInfo.iconBackgroundColor, is_valid: true, }, position: { x: px, y: py }, locked: true, }); }; const addRunCountButton = (event) => { // Count executions? // Maybe it shouldn't be onclick? } const addConditionDraggers = (event, allElements, branches) => { const nodedata = event.target.data() const position = event.target.position() var conditions = [] const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") try { conditions = JSON.parse(foundParam.value) } catch (e) { //toast("Failed parsing conditions: ", e) } // Test conditions if (conditions === undefined || conditions === null || typeof conditions !== "object") { return } // Look for if it has the "Else" condition or not const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") const parentId = nodedata.id // Force following of Else at the least const newId = uuidv5(parentId, uuidv5.URL) if (elseindex === -1) { conditions.push({ name: "Else", check: "Else", id: newId, parent_source: parentId, }) } else { conditions[elseindex].id = newId } // 4 conditions (with else) = 300px -> 75px each const parentHeight = (conditions.length*75)*0.75 var startheight = -parentHeight/2 var newnodes = [] for (let conditionkey in conditions) { var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id // Check if circleId is a valid uuid or not if (circleId === undefined || circleId === null) { circleId = uuidv4() } if (!isUUID(circleId)) { if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { circleId = uuidv5(conditions[circleId].name, uuidv5.URL) } else { circleId = uuidv4() conditions[conditionkey].name = circleId conditions[conditionkey].id = circleId } } // Check if circleId already exists as a node if (cy !== undefined && cy !== null) { const existingNode = cy.getElementById(circleId) if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { continue } } // 1. Create "small" nodes at each point along the section based on the amount of conditions // 2. Make these conditions have edgehandles // 3. Make these conditions have a "drag" handle const px = position.x + 65 const py = position.y + startheight console.log("Y height: ", startheight) const node = { group: "nodes", data: { name: conditions[conditionkey].name, id: circleId, buttonType: "condition-drag", attachedTo: nodedata.id, is_valid: true, }, position: { x: px, y: py, }, locked: true, } newnodes.push(node) // Check if ANY of the incoming branches has the id as source if (branches !== undefined && branches !== null && branches.length > 0) { for (let branchkey in branches) { const branch = branches[branchkey] if (branch.source_id !== circleId) { continue } const branchid = uuidv4() newnodes.push({ group: "edges", data: { id: branchid, _id: branchid, source: circleId, target: branch.destination_id, label: branch.label, conditions: branch.conditions, hasErrors: branch.has_errors, decorator: false, parent_source: parentId, } }) } } startheight = startheight + parentHeight/(conditions.length-1) } if (cy !== undefined && cy !== null) { cy.add(newnodes) } else { var newelements = elements if (allElements !== undefined) { newelements = allElements } for (let nodekey in newnodes) { newelements.push(newnodes[nodekey]) } console.log("ELEMENTS: ", newelements) setElements(newelements) } } const addCopyButton = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; const px = parentNode.position("x") - 65; const py = parentNode.position("y") - 5; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); const iconInfo = { icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); cy.add({ group: "nodes", data: { weight: 30, id: circleId, name: "TEEEXT", isButton: true, buttonType: "copy", attachedTo: event.target.data("id"), icon: svgpin_Url, iconBackground: iconInfo.iconBackgroundColor, is_valid: true, }, position: { x: px, y: py }, locked: true, }); }; const addActionSuggestions = (nodedata, event) => { if (nodedata.type !== "ACTION") { return } var parentNode = cy.$("#" + event.target.data("id")) if (parentNode.data("isButton") || parentNode.data("buttonId")) { return } const px = parentNode.position("x") + 0; const py = parentNode.position("y") + 100; const parentlabel = parentNode.data("label")?.toLowerCase().replace(" ", "_") const parentname = parentNode.data("app_name")?.toLowerCase().replace(" ", "_") if (!parentlabel?.startsWith(parentname)+"_") { return } // Check if action has changed const parentAppId = parentNode.data("app_id") const parentActionname = parentNode.data("name") for (var appkey in apps) { const curapp = apps[appkey] if (curapp.id !== parentAppId) { continue } if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { continue } var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) if (startIndex === -1) { startIndex = 0 } if (curapp.actions[startIndex].name !== parentActionname) { console.log("Return 2") return } break } console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") const iconInfo = { icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); // 1. Find the app // 2. Loop the apps' actions // 3. Find actions based on category label IF it exists var addedLabels = [] for (let appKey in apps) { const curapp = apps[appKey] if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { continue } if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { continue } for (let actionKey in curapp.actions) { const curaction = curapp.actions[actionKey] // Check if this is the current action already if (parentNode.data("name") == curaction.name) { continue } if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { if (addedLabels.includes(curaction.category_label[0])) { continue } if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { continue } cy.add({ group: "nodes", data: { weight: 30, id: uuidv4(), label: curaction.category_label[0], attachedTo: event.target.data("id"), is_valid: true, buttonType: "ACTIONSUGGESTION", }, position: { x: px, y: py + (addedLabels.length * 50), }, locked: true, }) addedLabels.push(curaction.category_label[0]) if (addedLabels.length >= 2) { break } } } break } } const addSuggestionButtons = (nodedata, event) => { //console.log("Skipping Adding suggestion buttons") //return // Skipping add for now. Should Re-enable // Add a button for autocompletion based on input if (nodedata.type === "ACTION") { /* const color = "#34a853" // Fix icon const iconInfo = { icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); const decoratorNode = { position: { x: event.target.position().x + 0, y: event.target.position().y + 65, }, locked: true, data: { isButton: true, isValid: true, is_valid: true, //label: "+", attachedTo: nodedata.id, imageColor: color, buttonType: "suggestion", icon: svgpin_Url, iconBackground: iconInfo.iconBackgroundColor, }, }; cy.add(decoratorNode); */ } if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { return } var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; const px = parentNode.position("x") + 0; const py = parentNode.position("y") + 200; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); var startHeight = 0 for (let recKey in workflowRecommendations) { const rec = workflowRecommendations[recKey] if (rec.action_id !== nodedata.id) { continue } if (rec.recommendations === undefined || rec.recommendations === null || rec.recommendations.length === 0) { continue } for (let recIndex in rec.recommendations) { const parsedRec = rec.recommendations[recIndex] console.log("REC: ", parsedRec) const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" const foundApp = apps.find((app) => app.app_name === parsedRec.app_name && app.app_version === foundVersion) // Find out if foundApp is shuffle tools, and if so, add the correct image based on name const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : foundApp === undefined || foundApp === null ? theme.palette.defaultImage : foundApp.large_image const uuid = uuidv4() const attachedToId = event.target.data("id") // Check if parsedRec.app_action exists already as a node under this one const branches = cy.edges().jsons() var found = false for (let branchKey in branches) { const branch = branches[branchKey] if (branch.data.source !== attachedToId) { continue } const targetNode = cy.getElementById(branch.data.target) if (targetNode === undefined || targetNode === null) { continue } if (targetNode.data("name") === parsedRec.app_action) { console.log("Found existing node (action name): ", targetNode) found = true } // FIXME: This could potentially be removed if (targetNode.data("app_id") === parsedRec.app_id) { console.log("Found existing node (id): ", targetNode) found = true } } // Skip the suggestion if it already exists if (found) { continue } // Checks for src/dst (e.g. trigger = src usually) const isTarget = true var name = parsedRec.app_action if (parsedRec.app_action === "subflow") { name = "Shuffle Workflow" } else if (parsedRec.app_action === "user_input") { name = "User Input" } const newaction = { name: name, label: parsedRec.app_action, label_replaced: parsedRec.app_action.replace("_", " ", -1), id: uuid, app_name: parsedRec.app_name, app_version: foundVersion, app_id: parsedRec.app_id, sharing: false, private_id: "", isStartNode: false, large_image: largeImage, is_valid: true, isSuggestion: true, isTarget: isTarget, attachedTo: attachedToId, finished: false, } cy.add({ group: "nodes", data: newaction, position: { x: px+startHeight, y: py, }, locked: true, }); cy.add({ group: "edges", data: { source: event.target.data("id"), target: uuid, decorator: true, } }) startHeight += 100 } console.log("Got Rec: ", rec) break } } const addDeleteButton2 = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; const px = parentNode.position("x") + 100; const py = parentNode.position("y") + 35; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); const iconInfo = { icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); cy.add({ group: "nodes", data: { weight: 30, id: circleId, name: "This is autocomplete", buttonType: "delete", attachedTo: event.target.data("id"), icon: svgpin_Url, iconBackground: iconInfo.iconBackgroundColor, is_valid: true, }, position: { x: px, y: py }, locked: true, }); }; const addDeleteButton = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; const px = parentNode.position("x") - 65; const py = parentNode.position("y") + 35; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); const iconInfo = { icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", iconColor: buttonColor, iconBackgroundColor: buttonBackgroundColor, }; const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); cy.add({ group: "nodes", data: { weight: 30, id: circleId, name: "TEEEXT", isButton: true, buttonType: "delete", attachedTo: event.target.data("id"), icon: svgpin_Url, iconBackground: iconInfo.iconBackgroundColor, is_valid: true, }, position: { x: px, y: py }, locked: true, }); }; const onNodeHover = (event) => { const nodedata = JSON.parse(JSON.stringify(event.target.data())) const cytoscapeElement = document.getElementById("cytoscape_view") if (cytoscapeElement !== undefined && cytoscapeElement !== null) { cytoscapeElement.style.cursor = "pointer" } sendStreamRequest({ "item": "node", "type": "hover", "id": nodedata.id, }) if (nodedata.finished === false) { console.log("NODE UNFINISHED (hover in): ", JSON.parse(JSON.stringify(nodedata))) // Should just be 1, so this should be fast enough :3 const incomingEdges = event.target.incomers("edge").jsons() if (incomingEdges !== undefined && incomingEdges !== null) { for (var i = 0; i < incomingEdges.length; i++) { // Find the actual edge const edge = cy.getElementById(incomingEdges[i].data.id) if (edge === undefined || edge === null) { console.log("edge is null or undefined") continue } // Set the edge to be dashed edge.style("target-arrow-color", "white") edge.style("line-style", "solid") edge.style("line-gradient-stop-colors", ["white", "white"]) } } return } //var parentNode = cy.$("#" + event.target.data("id")); //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (nodedata.app_name !== undefined && !workflow.public === true) { const allNodes = cy.nodes().jsons(); if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { var found = false; for (let nodekey in allNodes) { const currentNode = allNodes[nodekey]; if ( currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor ) { found = true; break; } } if (!found) { // Find how many executions it has var executions = 0 const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" const decoratorNode = { position: { x: event.target.position().x + 44, y: event.target.position().y + 44, }, locked: true, data: { isDescriptor: true, isValid: true, is_valid: true, isTrigger: true, label: `${matchingExecutions.length}`, attachedTo: nodedata.id, imageColor: color, hasExecutions: true, }, }; cy.add(decoratorNode) } } var found = false; for (var _key in allNodes) { const currentNode = allNodes[_key]; // console.log("CURRENT NODE: ", currentNode) if ((currentNode.data.buttonType === "ACTIONSUGGESTION" || currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { if (currentNode.data.buttonType === "condition-drag") { continue } cy.getElementById(currentNode.data.id).remove() } /*if ( currentNode.data.isSuggestion && currentNode.data.attachedTo !== nodedata.id ) { cy.getElementById(currentNode.data.id).remove(); }*/ if (currentNode.data.isButton && currentNode.data.attachedTo === nodedata.id) { found = true; } } if (nodedata.name === "switch") { addConditionDraggers(event) return } if (!found) { addDeleteButton(event) if (nodedata.type === "TRIGGER") { if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { addCopyButton(event); } else { // Check how many executions from the source addRunCountButton(event); } } else { addCopyButton(event); addStartnodeButton(event); } // autocomplete // right click // suggestions addActionSuggestions(nodedata, event); if (workflow.actions.length < 4) { addSuggestionButtons(nodedata, event); } else { //console.log("Too many actions to suggest (for now)") } } } if (nodedata.name === "switch") { return } var parsedStyle = { "border-width": "7px", "border-opacity": ".7", "font-size": "25px", //"cursor": "pointer", } if (nodedata.buttonType === "ACTIONSUGGESTION") { parsedStyle["font-size"] = "18px" } const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { const item = typeIds[idkey] if (item.data.id === nodedata.id) { //console.log("items: ", item.data.id, nodedata.id) parsedStyle["border-width"] = "12px" break } } if (nodedata.type !== "COMMENT") { parsedStyle.color = "white"; } if (event.target !== undefined && event.target !== null) { event.target.animate( { style: parsedStyle, }, { duration: animationDuration, } ); } const outgoingEdges = event.target.outgoers("edge"); const incomingEdges = event.target.incomers("edge"); if (outgoingEdges.length > 0) { outgoingEdges.addClass("hover-highlight"); } if (incomingEdges.length > 0) { outgoingEdges.addClass("hover-highlight"); } } const onEdgeHoverOut = (event) => { if (event === null || event === undefined || event.target === null || event.target === undefined) { event.target.removeStyle(); return; } const edgeData = event.target.data(); if (edgeData.decorator === true) { // Defaults event.target.style("target-arrow-color", "#555555") event.target.style("line-style", "dashed") event.target.style("line-gradient-stop-colors", ["#555555", "#555555"]) return; } const cytoscapeElement = document.getElementById("cytoscape_view") if (cytoscapeElement !== undefined && cytoscapeElement !== null) { cytoscapeElement.style.cursor = "default" } //event.target.removeStyle(); }; // This is here to have a proper transition for lines const onEdgeHover = (event) => { if (event === null || event === undefined || event.target === null || event.target === undefined) { return; } const edgeData = event.target.data(); if (edgeData.decorator === true) { // Set color of it to white and not stripled event.target.style("target-arrow-color", "white") event.target.style("line-style", "solid") event.target.style("line-gradient-stop-colors", ["white", "white"]) return; } // FIXME: Color problem. Do later //sendStreamRequest({ // "item": "edge", // "type": "hover", // "id": edgeData.id, //}) const cytoscapeElement = document.getElementById("cytoscape_view") if (cytoscapeElement !== undefined && cytoscapeElement !== null) { cytoscapeElement.style.cursor = "pointer" } //const sourcecolor = cy // .getElementById(event.target.data("source")) // .style("border-color"); //const targetcolor = cy // .getElementById(event.target.data("target")) // .style("border-color"); ////console.log(sourcecolor, targetcolor) //if ( // sourcecolor !== null && // sourcecolor !== undefined && // targetcolor !== null && // targetcolor !== undefined && // !sourcecolor.includes("rgb") && // !targetcolor.includes("rgb") //) { // console.log(sourcecolor) // console.log(targetcolor) // if (event.target !== null && event.target.value !== null) { // event.target.animate({ // style: { // "target-arrow-color": targetcolor, // "line-fill": "linear-gradient", // "line-gradient-stop-colors": [sourcecolor, targetcolor], // "line-gradient-stop-positions": [0, 1], // }, // duration: animationDuration, // }) // } else { // event.target.animate({ // style: { // "target-arrow-color": targetcolor, // "line-fill": "linear-gradient", // "line-gradient-stop-colors": ["#41dcab", "#41dcab"], // "line-gradient-stop-positions": [0, 1], // }, // duration: animationDuration, // }) // } //} if (event.target !== undefined && event.target !== null) { // If decorator and hovered // Set color to white //const targetcolor = "#66a8b1" //const parsedStyle = { // "width": "10px", // "font-size": "18px", // "target-arrow-color": "#66a8b1", // "color": "#66a8b1", //} //event.target.addClass("shuffle-hover-highlight"); //console.log("Style1: ", event.target) //console.log("Style: ", event.target.style()) //event.target.animate( // { // style: parsedStyle, // }, // { // duration: animationDuration, // } //) } } // Thanks to: // Calculates how a branch should curve (it's still weird~) // https://codepen.io/guillaumethomas/pen/xxbbBKO const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => { const xParsed = destinationnodePosition.x - sourcenodePosition.x const yParsed = destinationnodePosition.y - sourcenodePosition.y const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed) const costheta = xParsed / z const alpha = 0.3 var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta] var controlPointWeight = [alpha, 1 - alpha] //'control-point-weight': ['0.33', '0.66'], //var controlPointWeight = ["0.33", "0.66"] //var controlPointDistance = ["33%", "-66%"] //var controlPointWeight = ["0.00", "1.00"] /* if (yParsed !== 0) { //const degreeFound = Math.atan2(xParsed / yParsed) const degreeFound = Math.atan2(xParsed, yParsed) * 180 / Math.PI if (degreeFound > 90 && degreeFound < 180) { console.log("TOPRIGHT") } else if (degreeFound < 90 && degreeFound > 0) { console.log("BOTTOMRIGHT") } else if (degreeFound < 0 && degreeFound > -90) { console.log("BOTTOMLEFT") //controlPointWeight = ["0.20", "0.80"] //controlPointWeight = "0.7" //controlPointDistance = "50%" } else if (degreeFound < -90 && degreeFound > -180) { console.log("TOPLEFT") } else { console.log("STRAIGHT!") } } */ if (isNaN(controlPointDistance[0])) { controlPointDistance[0] = 0 } if (isNaN(controlPointDistance[1])) { controlPointDistance[1] = 0 } if (isNaN(controlPointWeight[0])) { controlPointWeight[0] = 0 } if (isNaN(controlPointWeight[1])) { controlPointWeight[1] = 0 } return { "distance": controlPointDistance, "weight": controlPointWeight, } } const setupGraph = (inputworkflow) => { // Reset cytoscape nodes and branches if (cy !== undefined && cy !== null) { if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { //cy.remove('*') } } if (inputworkflow.actions === undefined || inputworkflow.actions === null) { inputworkflow.actions = [] } if (inputworkflow.branches === undefined || inputworkflow.branches === null) { inputworkflow.branches = [] } if (inputworkflow.triggers === undefined || inputworkflow.triggers === null) { inputworkflow.triggers = [] } if (inputworkflow.comments === undefined || inputworkflow.comments === null) { inputworkflow.comments = [] } if (inputworkflow.visual_branches === undefined || inputworkflow.visual_branches === null) { inputworkflow.visual_branches = [] } const actions = inputworkflow.actions.map((action) => { const node = {}; if (!action.isStartNode && action.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(action) const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); action.large_image = svgpin_Url; action.fillGradient = iconInfo.fillGradient; action.fillstyle = "solid"; if ( action.fillGradient !== undefined && action.fillGradient !== null && action.fillGradient.length > 0 ) { action.fillstyle = "linear-gradient"; } else { action.iconBackground = iconInfo.iconBackgroundColor; } } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { const iconInfo = GetIconInfo(action) if (iconInfo !== undefined && iconInfo !== null) { action.fillGradient = iconInfo.fillGradient action.iconBackground = iconInfo.iconBackgroundColor action.fillstyle = "linear-gradient" } } node.position = action.position; node.data = action; node.data.id = action["id"]; node.data._id = action["id"]; node.data.type = "ACTION"; node.isStartNode = action["id"] === inputworkflow.start; if (inputworkflow.public === true) { node.data.is_valid = true node.is_valid = true } var example = ""; if ( action.example !== undefined && action.example !== null && action.example.length > 0 ) { example = action.example; } node.data.example = example; return node }) // What are these again? Where are they used? const decoratorNodes = [] /* // Removed for now as it wasn't really that helpful const decoratorNodes = inputworkflow.actions.map((action) => { if (!action.isStartNode) { if (action.app_name === "Testing") { return null } else if (action.app_name === "Shuffle Tools") { return null } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { return null } } if (action.id === undefined || action.id === null) { return null } if (action.position === undefined || action.position === null || action.position.x === undefined || action.position.x === null || action.position.y === undefined || action.position.y === null) { return null } const iconInfo = GetIconInfo(action); const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); const offset = action.isStartNode ? 36 : 44; const decoratorNode = { position: { x: action.position.x + offset, y: action.position.y + offset, }, locked: true, data: { isDescriptor: true, isValid: true, is_valid: true, label: "", image: svgpin_Url, imageColor: iconInfo.iconBackgroundColor, attachedTo: action.id, }, } return decoratorNode }) */ const foundtriggers = inputworkflow.triggers.map((trigger) => { const node = {}; node.position = trigger.position; if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) { // Search triggers array for it where the name is matching and set image var foundTrigger = triggers.find((t) => t.name === trigger.name) if (foundTrigger !== undefined && foundTrigger !== null) { console.log("Autofilled missing trigger image") trigger.large_image = foundTrigger.large_image } } node.data = trigger; node.data._id = trigger["id"]; node.data.id = trigger["id"]; node.data.type = "TRIGGER"; return node; }); var comments = []; if ( inputworkflow.comments !== undefined && inputworkflow.comments !== null && inputworkflow.comments.length > 0 ) { comments = inputworkflow.comments.map((comment) => { const node = {}; node.position = comment.position; node.data = comment; node.data._id = comment["id"]; node.data.type = "COMMENT"; return node; }); } var insertedNodes = [].concat(actions, foundtriggers, decoratorNodes, comments); insertedNodes = insertedNodes.filter((node) => node !== null); var edges = inputworkflow.branches.map((branch, index) => { const edge = {}; var conditions = inputworkflow.branches[index].conditions; if (conditions === undefined || conditions === null) { conditions = []; } var label = ""; if (conditions.length === 1) { label = conditions.length + " condition"; } else if (conditions.length > 1) { label = conditions.length + " conditions"; } // Verify if branch.source_id and branch.destination_id exists in triggers or actions /* var sourceExists = false; var destinationExists = false; for (var i = 0; i < insertedNodes.length; i++) { console.log("Insertednode: ", insertedNodes[i].data); if (insertedNodes[i].data._id === branch.source_id) { sourceExists = true; } if (insertedNodes[i].data._id === branch.destination_id) { destinationExists = true; } } if (sourceExists === false || destinationExists === false) { console.log("Couldn't find source node for branch " + branch.id); return null; } */ var parentcontrolled = false if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { parentcontrolled = true } edge.data = { id: branch.id, _id: branch.id, source: branch.source_id, target: branch.destination_id, label: label, conditions: conditions, hasErrors: branch.has_errors, decorator: false, parent_controlled: parentcontrolled, } // This is an attempt at prettier edges. The numbers are weird to work with. // Bezier curves //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html var sourcenode = actions.find(node => node.data._id === branch.source_id || node.data.id === branch.source_id) var destinationnode = actions.find(node => node.data._id === branch.destination_id || node.data.id === branch.destination_id) if (sourcenode === undefined) { sourcenode = foundtriggers.find(node => node.data._id === branch.source_id || node.data.id === branch.source_id) } if (destinationnode === undefined) { destinationnode = foundtriggers.find(node => node.data._id === branch.destination_id || node.data.id === branch.destination_id) } if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { const edgeCurve = calculateEdgeCurve(sourcenode.position, destinationnode.position) edge.style = { 'control-point-distance': edgeCurve.distance, 'control-point-weight': edgeCurve.weight, } } else { console.log("FAILED node curve handling") } return edge; }); if (inputworkflow.visual_branches !== undefined && inputworkflow.visual_branches !== null && inputworkflow.visual_branches.length > 0) { const visualedges = inputworkflow.visual_branches.map((branch, index) => { const edge = {}; if (inputworkflow.branches[index] === undefined) { return {} } var conditions = inputworkflow.branches[index].conditions if (conditions === undefined || conditions === null) { conditions = []; } const label = "Subflow"; edge.data = { id: branch.id, _id: branch.id, source: branch.source_id, target: branch.destination_id, label: label, decorator: true, } return edge }); edges = edges.concat(visualedges) } // Verifies if a branch is valid and skips others var newedges = []; for (var edgeKey in edges) { var item = edges[edgeKey]; if (item.data === undefined) { continue; } const sourcecheck = insertedNodes.find( (data) => data.data.id === item.data.source ); const destcheck = insertedNodes.find( (data) => data.data.id === item.data.target ); if (sourcecheck === undefined || destcheck === undefined) { continue; } newedges.push(item); } insertedNodes = insertedNodes.concat(newedges); setWorkflow(inputworkflow); // Reset view for cytoscape if (cy !== undefined && cy !== null) { cy.add(insertedNodes); cy.fit(null, 200); } else { setElements(insertedNodes); } const additionalNodes = inputworkflow.actions.map((action) => { // Looking for: el.data("name") != "switch" if (action.name !== "switch") { return null } addConditionDraggers({ target: { // Run data() function data: function() { return action }, position: function() { return action.position } } }, insertedNodes, inputworkflow.branches, ) }) } const removeNode = (nodeId) => { const selectedNode = cy.getElementById(nodeId); // console.log("selected node in removenode", selectedNode); if (selectedNode.data() === undefined) { console.log("No node to remove") return; } //console.log("Removing node: ", selectedNode.data("id"), "Action: ", selectedAction.id) // Get selected node if (selectedNode.data().type === "TRIGGER") { const triggerindex = workflow.triggers.findIndex( (data) => data.id === selectedNode.data().id ); setSelectedTriggerIndex(triggerindex); if (selectedNode.data().trigger_type === "SCHEDULE") { setSelectedTrigger(selectedNode.data()); stopSchedule(selectedNode.data(), triggerindex); } else if (selectedNode.data().trigger_type === "WEBHOOK") { setSelectedTrigger(selectedNode.data()); deleteWebhook(selectedNode.data(), triggerindex); } else if (selectedNode.data().trigger_type === "EMAIL") { setSelectedTrigger(selectedNode.data()); stopMailSub(selectedTrigger, triggerindex); } else if (selectedNode.data().trigger_type === "PIPELINE") { setSelectedTrigger(selectedNode.data()); const pipelineConfig = { command: "", name: selectedNode.data().label, type: "delete", environment: selectedNode.data().environment, workflow_id: workflow.id, trigger_id: selectedNode.data().id, start_node: "", }; submitPipeline(selectedNode.data(), triggerindex, pipelineConfig); } } //if (selectedNode.data("id") === selectedAction.id) { // setSelectedApp({}); // setSelectedAction({}); //setSelectedTrigger({}); //setSelectedTriggerIndex({}); //} const parsedSelection = cy.$(":selected"); if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { toast("This node can't be deleted."); } else { selectedNode.remove(); setSelectedTrigger({}) setSelectedEdge({}) setSelectedAction({}) } // An attempt at NOT unselecting when removing /* setTimeout(() => { if (parsedSelection.data() !== undefined) { if (parsedSelection.data("id") !== selectedNode.data("id")) { console.log("SHOULD SELECT SINCE ID IS DIFFERENT") parsedSelection.select() } } console.log("Parsed: ", parsedSelection.data("id"), selectedNode.data("id")) }, 2500) */ }; if (isLoaded && setupSent === false) { setSetupSent(true) sendStreamRequest({ "item": "workflow", "type": "enter", "id": workflow.id, }) } const fetchRecommendations = (inputWorkflow) => { console.log("Disabled recommendations as they were too inaccurate") return const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) fetch(globalUrl + "/api/v1/workflows/recommend", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(parsedWorkflow), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for usecases"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { //console.log("recommendations: ", responseJson); if (responseJson.actions !== undefined && responseJson.actions !== null) { console.log("Got recommendations: ", responseJson.actions) //if (cy !== undefined && cy !== null) { // cy.removeListener("mouseover", "node"); //} setWorkflowRecommendations(responseJson.actions) //if (cy !== undefined && cy !== null) { // cy.on("mouseover", "node", (e) => onNodeHover(e)); //} } else { setWorkflowRecommendations([]) } } else { setWorkflowRecommendations([]) } }) .catch((error) => { //toast("ERROR: " + error.toString()); setWorkflowRecommendations([]) console.log("ERROR getting usecases: " + error.toString()); }) } const fetchUsecases = () => { fetch(globalUrl + "/api/v1/workflows/usecases", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for usecases"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { setUsecases(responseJson) } else { } }) .catch((error) => { //toast("ERROR: " + error.toString()); console.log("ERROR getting usecases: " + error.toString()); }) } const getRevisionHistory = (workflow_id) => { fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!"); } // Read text from stream //return response.text(); return response.json(); }) .then((responseJson) => { if (responseJson === null) { console.log("No revisions found") return } if (responseJson.success === false) { console.log("Error getting workflow revisions: ", responseJson) return } setAllRevisions(responseJson) setSelectedVersion(responseJson[0]) }) .catch((error) => { console.log("Error getting workflow revisions: ", error) }); } const loadTriggers = () => { const url = `${globalUrl}/api/v1/triggers` fetch(url, { method: "GET", headers: { "content-type": "application/json" }, credentials: "include", } ) .then((response) => { if (response.status !== 200) { throw new Error("No folders :o!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { setAllTriggers(responseJson) } }) .catch((error) => { console.log("Get outlook folders error: ", error.toString()); }); } // eslint-disable-next-line react-hooks/exhaustive-deps //useEffect(() => { if (firstrequest) { setFirstrequest(false); getWorkflow(props.match.params.key, {}); getChildWorkflows(props.match.params.key) getRevisionHistory(props.match.params.key) loadTriggers() getApps() fetchUsecases() const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; // FIXME: Don't check specific one here const tmpExec = new URLSearchParams(cursearch).get("execution_highlight"); if ( tmpExec !== undefined && tmpExec !== null && tmpExec === "executions" ) { setExecutionModalOpen(true) const newitem = removeParam("execution_highlight", cursearch); navigate(curpath + newitem) //props.history.push(curpath + newitem); } const tmpView = new URLSearchParams(cursearch).get("view"); if ( tmpView !== undefined && tmpView !== null && tmpView === "executions" ) { setExecutionModalOpen(true); const newitem = removeParam("view", cursearch); navigate(curpath + newitem) //navigate(`?execution_highlight=${parsed_url}`) //props.history.push(curpath + newitem); } return; } // App length necessary cus of cy initialization // Not using recommendations, so skipping this for now //if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0 && workflowRecommendations !== undefined) { if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { setGraphSetup(true); setupGraph(workflow); console.log("In graph setup") // 2nd load - configures cytoscape //} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) { } else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined) { //This part has to load LAST, as it's kind of not async. //This means we need everything else to happen first. setEstablished(true); // Validate if the node is just a node lol // https://www.npmjs.com/package/cytoscape-grid-guide // if (cy.gridGuide !== undefined) { cy.gridGuide({ gridSpacing: 30, guidelinesStyle: { strokeStyle: "#8b7d6b", // color of geometric guidelines geometricGuidelineRange: 400, // range of geometric guidelines range: 100, // max range of distribution guidelines minDistRange: 10, // min range for distribution guidelines distGuidelineOffset: 10, // shift amount of distribution guidelines horizontalDistColor: "#ff0000", // color of horizontal distribution alignment verticalDistColor: "#00ff00", // color of vertical distribution alignment initPosAlignmentColor: "#0000ff", // color of alignment to initial mouse location lineDash: [0, 0], // line style of geometric guidelines horizontalDistLine: [0, 0], // line style of horizontal distribution guidelines verticalDistLine: [0, 0], // line style of vertical distribution guidelines initPosAlignmentLine: [0, 0], // line style of alignment to initial mouse position } }) } else { console.log("ERROR: Failed to render grid as it's unitialized") } if (cy.edgehandles !== undefined) { cy.edgehandles({ handleNodes: (el) => { if (el.isNode() && el.data("buttonType") != "ACTIONSUGGESTION" && el.data("name") != "switch" && !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && el.data("type") !== "COMMENT") { return true } return false }, preview: false, toggleOffOnLeave: true, loopAllowed: function (node) { return false; }, }) //cy.edgehandles({ // preview: false, //}) //cy.edgehandles().enable() } else { console.log("ERROR: Failed to initialize edgehandler") } // preview: true, cy.fit(null, 200); cy.on("boxselect", "node", (e) => { if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) { e.target.unselect(); } e.target.addClass("selected"); }); cy.on("boxstart", (e) => { console.log("START"); }); cy.on("boxend", (e) => { console.log("END: ", cy) var cydata = cy.$(":selected").jsons(); if (cydata !== undefined && cydata !== null && cydata.length > 0) { toast(`Selected ${cydata.length} element(s). CTRL+C to copy them.`); } }); cy.on('grab', 'edge', (e) => { console.log("Edge grabbed: ", e.target.data()) }) cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication); }); cy.on("select", "edge", (e) => onEdgeSelect(e)); cy.on("unselect", (e) => onUnselect(e)); cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); cy.on("remove", "node", (e) => onNodeRemoved(e)); cy.on("remove", "edge", (e) => onEdgeRemoved(e)); cy.on("mouseover", "edge", (e) => onEdgeHover(e)); cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); cy.on("mouseover", "node", (e) => onNodeHover(e)); cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); // Handles dragging cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); cy.on("cxttap", "node", (e) => onCtxTap(e)); document.title = "Workflow - " + workflow.name; startWorkflowStream(props.match.params.key); registerKeys(); } //}) const stopSchedule = (trigger, triggerindex) => { fetch( `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule/${trigger.id}`, { method: "DELETE", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", } ) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } return response.json(); }) .then((responseJson) => { // No matter what, it's being stopped. if (!responseJson.success) { if (responseJson.reason !== undefined) { toast("Failed to stop schedule: " + responseJson.reason); } } else { toast("Successfully stopped schedule"); } workflow.triggers[triggerindex].status = "stopped"; trigger.status = "stopped"; setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow) }) .catch((error) => { console.log("Stop schedule error: ", error.toString()) }) } const submitPipeline = (trigger, triggerindex, usecase) => { if (trigger.name.length <= 0) { toast("Error: name can't be empty"); return; } var mappedStartnode = ""; const alledges = cy.edges().jsons(); if (alledges !== undefined && alledges !== null && alledges.length > 0) { for (let edgekey in alledges) { const tmp = alledges[edgekey]; if (tmp.data.source === trigger.id) { mappedStartnode = tmp.data.target; break; } } } const data = usecase; data.start_node = mappedStartnode const url = `${globalUrl}/api/v1/triggers/pipeline`; fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(data), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } return response.json(); }) .then((responseJson) => { if (!responseJson.success && data.type !== "delete") { toast("Failed to set pipeline: " + responseJson.reason); } else { if (data.type === "create") { toast("Pipeline will be created!"); } else if (data.type === "stop") { toast("Pipeline will be stopped!"); } else { toast("Pipeline deleted!") return } if (trigger.parameters){ trigger.parameters.push({ name: data.name, value: data.command, });} if (data.type === "stop") trigger.status = "stopped"; else trigger.status = "running"; workflow.triggers[triggerindex] = trigger; setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow); } }) .catch((error) => { console.log("Get pipeline error: ", error.toString()); }); }; const submitSchedule = (trigger, triggerindex) => { if (trigger.name.length <= 0) { toast("Error: name can't be empty"); return; } var mappedStartnode = "" const alledges = cy.edges().jsons() if (alledges !== undefined && alledges !== null && alledges.length > 0) { for (let edgekey in alledges) { const tmp = alledges[edgekey] if (tmp.data.source === trigger.id) { mappedStartnode = tmp.data.target break } } } toast("Creating schedule") const data = { name: trigger.name, frequency: workflow.triggers[triggerindex].parameters[0].value, execution_argument: workflow.triggers[triggerindex].parameters[1].value, environment: workflow.triggers[triggerindex].environment, id: trigger.id, start: mappedStartnode, } fetch( `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(data), credentials: "include", } ) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { toast("Failed to set schedule: " + responseJson.reason); } else { toast("Successfully created schedule"); workflow.triggers[triggerindex].status = "running"; trigger.status = "running"; setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow); } }) .catch((error) => { //toast(error.toString()); console.log("Get schedule error: ", error.toString()); }); }; const getSigmaInfo = () => { const url = globalUrl + "/api/v1/files/detection/sigma_rules"; fetch(url, { method: "GET", credentials: "include", headers: { "Content-Type": "application/json", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { toast("Failed to get sigma rules"); } else { setRules(responseJson.sigma_info); } }) ) .catch((error) => { console.log("Error in getting sigma files: ", error); toast("An error occurred while fetching sigma rules"); }); }; const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 50 const appViewStyle = { marginLeft: 5, marginRight: 5, display: "flex", flexDirection: "column", minHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, maxHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, }; const paperAppStyle = { borderRadius: theme.palette?.borderRadius, minHeight: isMobile ? 50 : 70, maxHeight: isMobile ? 50 : 70, minWidth: isMobile ? 50 : "100%", maxWidth: isMobile ? 50 : "100%", marginTop: "5px", color: "white", backgroundColor: theme.palette.surfaceColor, cursor: "pointer", display: "flex", }; const paperVariableStyle = { borderRadius: theme.palette?.borderRadius, minHeight: 50, maxHeight: 50, minWidth: "100%", maxWidth: "100%", marginTop: "5px", color: "white", backgroundColor: theme.palette.surfaceColor, cursor: "pointer", display: "flex", } const VariableItem = (props) => { const { variable, index, type } = props; const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const menuClick = (event) => { setOpen(!open); setAnchorEl(event.currentTarget); }; const deleteVariable = (type, variableIndex) => { console.log("Delete type: ", type, variableIndex) if (type === "normal") { if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) vars.splice(variableIndex, 1) workflow.workflow_variables = vars console.log("Workflow after del: ", workflow) setWorkflow(workflow); setUpdate(Math.random()); } } else if (type === "exec") { if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) vars.splice(variableIndex, 1) workflow.execution_variables = vars console.log("Workflow after del: ", workflow) setWorkflow(workflow); setUpdate(Math.random()); } } }; return (
{ }}>
{ setVariableInfo({ "name": variable.name, "description": variable.description, "value": variable.value, "index": index, }) if (type === "normal") { setVariablesModalOpen(true); } else if (type === "exec") { setExecutionVariablesModalOpen(true); } else { console.log("Unknown type: ", type) } }} > Name: {variable.name}
{ setOpen(false); setAnchorEl(null); }} > { setOpen(false); setVariableInfo({ "name": variable.name, "description": variable.description, "value": variable.value, "index": index, }) if (type === "normal") { setVariablesModalOpen(true); } else if (type === "exec") { setExecutionVariablesModalOpen(true); } else { console.log("Unknown type: ", type) } }} key={"Edit"} > {"Edit"} { deleteVariable(type, index); setOpen(false); }} key={"Delete"} > {"Delete"}
) } const VariablesView = () => { const variableScrollStyle = { margin: 15, overflow: "scroll", height: isMobile ? "100%" : "66vh", overflowX: "auto", overflowY: "auto", flex: "10", }; return (
What are{" "} Workflow variables? {workflow.workflow_variables === null ? null : workflow.workflow_variables.map((variable, varindex) => { return ( ); })}
What are{" "} Runtime variables? {workflow.execution_variables === null || workflow.execution_variables === undefined ? null : workflow.execution_variables.map((variable, varindex2) => { return ( ); })}
); }; const HandleLeftView = () => { // console.log("HandleLeftView Rendered!") const handleSetTab = (event, newValue) => { setCurrentView(newValue); }; // Defaults to apps. var thisview = ( ); if (currentView === 1) { thisview = ; } else if (currentView === 2) { thisview = ; } const tabStyle = { maxWidth: isMobile ? leftBarSize : leftBarSize / 3, minWidth: isMobile ? leftBarSize : leftBarSize / 3, flex: 1, textTransform: "none", }; const iconStyle = { marginTop: 3, marginRight: 5, }; return (
{thisview}
{isMobile ? null : Apps} } style={tabStyle} /> {isMobile ? null : Triggers} } style={{ maxWidth: isMobile ? leftBarSize : leftBarSize / 3, minWidth: isMobile ? leftBarSize : leftBarSize / 3, flex: 1, textTransform: "none", padding: 0, }} /> {isMobile ? null : Vars} } style={tabStyle} />
) } const TriggersView = () => { const triggersViewStyle = { marginLeft: 10, marginRight: 10, display: "flex", flexDirection: "column", } // Predefined hurr return (
{triggers.map((trigger, index) => { // Hiding since March 2024 if (trigger.trigger_type === "EMAIL") { return null } const imagesize = isMobile ? 40 : trigger.large_image.includes("svg") ? 50 : 50 var imageline = trigger.large_image.length === 0 ? : const title = trigger.trigger_type === "WEBHOOK" ? "Workflow starters" : trigger.trigger_type === "SUBFLOW" ? "Mid-Workflow" : "" const color = trigger.is_valid ? green : yellow; return ( {title.length > 0 ? {title} : null} { handleTriggerDrag(e, trigger); }} onStop={(e) => { handleDragStop(e); }} dragging={false} position={{ x: 0, y: 0, }} > { }}>
{imageline} {isMobile ? null : {trigger.name} {trigger.description} }
); })}
); }; var newNodeId = ""; var parsedApp = {}; const handleTriggerDrag = (e, data) => { const cycontainer = cy.container(); // Chrome lol if ( e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft + cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop + cycontainer.offsetHeight ) { if (newNodeId.length > 0) { var currentnode = cy.getElementById(newNodeId); if (currentnode.length === 0) { return; } currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft); currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop); } else { if (workflow.start === "" || workflow.start === undefined) { toast("Define a starting action first."); return; } const triggerLabel = getNextActionName(data.name); newNodeId = uuidv4(); const newposition = { x: e.pageX - cycontainer.offsetLeft, y: e.pageY - cycontainer.offsetTop, }; var newAppData = { app_name: data.name, app_version: "1.0.0", environment: isCloud ? "cloud" : data.environment, description: data.description, long_description: data.long_description, errors: [], id_: newNodeId, _id_: newNodeId, id: newNodeId, finished: false, label: triggerLabel, type: data.type, is_valid: true, trigger_type: data.trigger_type, large_image: data.large_image, status: "uninitialized", name: data.name, isStartNode: false, position: newposition, } if (data.name === "Pipelines") { var newenv = environments.find((env) => { if (env.archived) { return false } if (env.Name === "cloud" || env.Type === "cloud") { return false } return true }) console.log("NEWENV: ", environments, newenv) if (newenv !== undefined && newenv !== null) { newAppData.environment = newenv.Name } else { newAppData.environment = "" } } console.log("NEW DATA: ", newAppData) // Can all the data be in here? hmm const nodeToBeAdded = { group: "nodes", data: newAppData, renderedPosition: newposition, }; cy.add(nodeToBeAdded); parsedApp = nodeToBeAdded; return; } } }; const handleDragStop = (e, app) => { var currentnode = cy.getElementById(newNodeId); if ( currentnode === undefined || currentnode === null || currentnode.length === 0 ) { return; } if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) { toast("Failed to add node. Please try again.") console.log("Failed to add node. Please try again. Parsed app:", parsedApp) return } // Using remove & replace, as this triggers the function // onNodeAdded() with this node after it's added currentnode.remove(); parsedApp.data.finished = true; parsedApp.data.position = currentnode.renderedPosition(); parsedApp.position = currentnode.renderedPosition(); parsedApp.renderedPosition = currentnode.renderedPosition(); var newAppData = parsedApp.data; if (newAppData.type === "ACTION") { //const activateApp = (appid) => { if (newAppData.activated === false) { activateApp(newAppData.app_id, false) } // AUTHENTICATION if (app.authentication !== undefined && app.authentication !== null && app.authentication.required === true) { // Setup auth here :) const authenticationOptions = []; var findAuthId = ""; if ( newAppData.authentication_id !== null && newAppData.authentication_id !== undefined && newAppData.authentication_id.length > 0 ) { findAuthId = newAppData.authentication_id; } const tmpAuth = JSON.parse(JSON.stringify(appAuthentication)); for (let authkey in tmpAuth) { if (authkey === undefined) { continue } var item = tmpAuth[authkey]; const newfields = {}; for (let fieldkey in item.fields) { if (item.fields[fieldkey] === undefined) { console.log("Problem with filterkey in Node select", fieldkey) continue } const filterkey = item.fields[fieldkey]["key"] if (filterkey === null || filterkey === undefined) { console.log("Problem with filterkey 2. Null or undefined 3") continue } newfields[filterkey] = item.fields[fieldkey]["value"]; } item.fields = newfields; if (item.app.id === app.id || item.app.name === app.name) { authenticationOptions.push(item); if (item.id === findAuthId) { newAppData.selectedAuthentication = item newAppData.authentication_id = item.id } else if (findAuthId === "") { // Will always be set to the last one if one isn't found. // Last = timestamp too newAppData.selectedAuthentication = item newAppData.authentication_id = item.id } } } if ( authenticationOptions !== undefined && authenticationOptions !== null && authenticationOptions.length > 0 ) { for (let authkey in authenticationOptions) { const option = authenticationOptions[authkey]; if (option.active && newAppData.authentication_id === "") { newAppData.selectedAuthentication = option; newAppData.authentication_id = option.id; break; } } } } else { newAppData.authentication = []; newAppData.authentication_id = ""; newAppData.selectedAuthentication = {}; } parsedApp.data = newAppData; cy.add(parsedApp); } else if (newAppData.type === "TRIGGER") { cy.add(parsedApp); } newNodeId = ""; parsedApp = {}; }; const barHeight = bodyHeight - appBarSize - 50; const appScrollStyle = { overflow: "scroll", maxHeight: isMobile ? bodyHeight - appBarSize * 4 : barHeight, minHeight: isMobile ? bodyHeight - appBarSize * 4 : barHeight, marginTop: 1, overflowY: "auto", overflowX: "hidden", } const handleAppDrag = (e, app) => { const cycontainer = cy.container(); // Handling drag of public apps if (app.objectID !== undefined && app.objectID !== null && app.objectID.length > 0) { // FIXME: This is still buggy for now. Not allowed return loadAppConfig(app.objectID, undefined) // Some arbitrary check app.id = app.objectID app.actions = [{ "name": "tmp", "parameters": [{ "name": "tmp", }] }] } if (app.type === "TRIGGER") { handleTriggerDrag(e, app) return } //console.log("e: ", e) //console.log("Offset: ", cycontainer) // HTML -> Canvas overlap check if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft + cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop + cycontainer.offsetHeight) { if (newNodeId.length > 0) { var currentnode = cy.getElementById(newNodeId); if ( currentnode === undefined || currentnode === null || currentnode.length === 0 ) { return; } currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft) currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop) } else { if (workflow.public) { console.log("workflow is public - not adding") return; } if (app.actions === undefined || app.actions === null) { app.actions = [] } /* if (app.actions === undefined || app.actions === null || app.actions.length === 0) { toast("App " + app.name + " currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.") return } */ newNodeId = uuidv4(); const actionType = "ACTION"; const actionLabel = getNextActionName(app.name); var parameters = null; var example = ""; var description = "" const startIndex = app.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) const actionIndex = startIndex < 0 ? 0 : startIndex if (app.actions[actionIndex] === undefined || app.actions[actionIndex] === null) { if (app.id !== undefined && app.id !== null) { loadAppConfig(app.id, false) } console.log("No actions found for app: ", app) return } // Make the first action the most relevant one for them based on previous use if (app.actions[actionIndex].parameters !== undefined && app.actions[actionIndex].parameters !== null && app.actions[actionIndex].parameters.length > 0) { parameters = JSON.parse(JSON.stringify(app.actions[actionIndex].parameters)) for (let paramkey in parameters) { // Check if parameter.name == "headers" and if it includes "=undefined". If it does, set the value to example if it exists, otherwise empty if (parameters[paramkey].name === "headers" && parameters[paramkey].value.includes("=undefined")) { if (parameters[paramkey].example !== undefined && parameters[paramkey].example !== null && parameters[paramkey].example.length > 0) { parameters[paramkey].value = parameters[paramkey].example } else { parameters[paramkey].value = "" } } } } if ( app.actions[actionIndex].returns !== undefined && app.actions[actionIndex].returns !== null && app.actions[actionIndex].returns.example !== undefined && app.actions[actionIndex].returns.example !== null && app.actions[actionIndex].returns.example.length > 0 ) { example = app.actions[actionIndex].returns.example; } if ( app.actions[actionIndex].description !== undefined && app.actions[actionIndex].description !== null && app.actions[actionIndex].description.length > 0 ) { description = app.actions[actionIndex].description } var parsedEnvironments = environments === undefined || environments === null || environments === [] ? isCloud ? "cloud" : "Shuffle" : environments[defaultEnvironmentIndex] === undefined ? isCloud ? "cloud" : "Shuffle" : environments[defaultEnvironmentIndex].Name // Basic automatic auth mapping var authId = "" if (appAuthentication !== undefined && appAuthentication !== null && appAuthentication.length > 0) { const appname = app.name.toLowerCase().replace(" ", "_") for (var key in appAuthentication) { const authKey = appAuthentication[key] if (authKey.app.id === app.id) { authId = authKey.id break } const appauthname = authKey.app.name.toLowerCase().replace(" ", "_") if (appauthname === appname) { authId = authKey.id } } } // List other nodes in the workflow and see if they have an environment set. If they do, use that as the default if (cy !== undefined && cy !== null) { const foundnodes = cy.nodes().jsons() if (foundnodes !== undefined && foundnodes !== null && foundnodes.length > 0) { // As they should all be the same, this is just an override for (let nodekey in foundnodes) { const curnode = foundnodes[nodekey] if (curnode.data.environment !== undefined && curnode.data.environment !== null && curnode.data.environment.length > 0) { parsedEnvironments = curnode.data.environment break } } } } const newAppData = { name: app.actions[actionIndex].name, label: actionLabel, app_name: app.name, app_version: app.app_version, app_id: app.id, sharing: app.sharing, private_id: app.private_id, description: description, environment: parsedEnvironments, errors: [], finished: false, id_: newNodeId, _id_: newNodeId, id: newNodeId, is_valid: true, type: actionType, parameters: parameters, isStartNode: false, large_image: app.large_image, run_magic_output: false, authentication: [], execution_variable: undefined, example: example, required_body_fields: app.actions[actionIndex].required_body_fields, category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "", authentication_id: authId, finished: false, template: app.template === true ? true : false, } // FIXME: Something is going wrong with params if (!isCloud && (!app.is_valid || (!app.activated && app.generated))) { console.log("NOT VALID: Activate!") activateApp(app.id, false) } // find the cytoscape offset position // Can this be done with zoom calculations? const nodeToBeAdded = { group: "nodes", data: JSON.parse(JSON.stringify(newAppData)), renderedPosition: { x: e.pageX - cycontainer.offsetLeft, y: e.pageY - cycontainer.offsetTop, }, }; parsedApp = nodeToBeAdded; cy.add(nodeToBeAdded); return; } } }; const AppView = (props) => { const { allApps, prioritizedApps, filteredApps, extraApps } = props; // console.log("AppView Rendered!") //extraApps, const [visibleApps, setVisibleApps] = React.useState( Array.prototype.concat.apply( prioritizedApps, Array.prototype.concat.apply( filteredApps.filter((innerapp) => !internalIds.includes(innerapp.id.toLowerCase())), triggers ) ) ) var delay = -75 var runDelay = false const ParsedAppPaper = (props) => { const app = props.app; const [hover, setHover] = React.useState(false); if (app.type !== "TRIGGER" && (app.id === "" || app.name === "")) { return null } const maxlen = 35 var newAppname = app.name newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1) if (newAppname.length > maxlen) { newAppname = newAppname.slice(0, maxlen) + ".." } newAppname = newAppname.replaceAll("_", " ") if (app.large_image === undefined || app.large_image === null || app.large_image === "") { app.large_image = theme.palette.defaultImage } const image = app.large_image !== undefined && app.large_image !== null && app.large_image !== "" ? app.large_image : theme.palette.defaultImage const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) const pixelSize = !hover ? "2px" : "4px"; //newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 && !(app.activated && app.generated) newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 ? `${pixelSize} solid ${green}` : `${pixelSize} solid ${yellow}`; if (app.id == highlightedApp && app.id !== "") { //console.log("Found correct appid to highlight: ", app.id) newAppStyle.border = "3px solid " + green } if (!app.activated && app.generated) { newAppStyle.borderLeft = `${pixelSize} solid ${yellow}`; } return ( { handleAppDrag(e, app); }} onStop={(e) => { handleDragStop(e, app); }} key={app.id} dragging={false} position={{ x: 0, y: 0, }} > { e.preventDefault() e.stopPropagation() setHover(true) if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { loadAppConfig(app.id, false) } }} onMouseOut={() => { setHover(false); }} onClick={() => { if (isMobile) { newNodeId = uuidv4(); const actionType = "ACTION"; const actionLabel = getNextActionName(app.name); var parameters = null; var example = ""; var description = "" if ( app.actions[0].parameters !== null && app.actions[0].parameters.length > 0 ) { parameters = app.actions[0].parameters; } if ( app.actions[0].returns.example !== undefined && app.actions[0].returns.example !== null && app.actions[0].returns.example.length > 0 ) { example = app.actions[0].returns.example; } if ( app.actions[0].description !== undefined && app.actions[0].description !== null && app.actions[0].description.length > 0 ) { description = app.actions[0].description } const parsedEnvironments = environments === null || environments === [] ? "cloud" : environments[defaultEnvironmentIndex] === undefined ? "cloud" : environments[defaultEnvironmentIndex].Name; // activated: app.generated === true ? app.activated === false ? false : true : true, const newAppData = { app_name: app.name, app_version: app.app_version, app_id: app.id, sharing: app.sharing, private_id: app.private_id, description: description, environment: parsedEnvironments, errors: [], finished: false, id_: newNodeId, _id_: newNodeId, id: newNodeId, is_valid: true, label: actionLabel, type: actionType, name: app.actions[0].name, parameters: parameters, isStartNode: false, large_image: image, run_magic_output: false, authentication: [], execution_variable: undefined, example: example, category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "", authentication_id: "", finished: false, }; const nodeToBeAdded = { group: "nodes", data: newAppData, renderedPosition: { x: 100, y: 100, }, }; parsedApp = nodeToBeAdded; cy.add(nodeToBeAdded); } }} > { if (props.index !== undefined && props.index !== null) { // Replace the image with the default image const foundImage = document.getElementById(`image_${props.index}`) if (foundImage !== undefined && foundImage !== null) { foundImage.src = theme.palette.defaultImage app.large_image = theme.palette.defaultImage } } }} style={{ pointerEvents: "none", userDrag: "none", userSelect: "none", borderRadius: theme.palette?.borderRadius, height: isMobile ? 40 : 55, width: isMobile ? 40 : 55, }} /> {isMobile ? null : 20 ? -1 : 12, fontSize: 19, }} > {newAppname} } ); }; const runSearch = (value) => { value = value.trim().toLowerCase() if (value.length > 0) { // Dedup based on name const preppedApps = Array.prototype.concat.apply(allApps, triggers).filter((app, index, self) => { return index === self.findIndex((t) => ( t.name === app.name )) }) var newApps = preppedApps.filter( (app) => app.name.toLowerCase().includes(value) || app.description.toLowerCase().includes(value) ) // Extend search if (newApps.length === 0) { newApps = allApps.filter((app) => { if (app.actions !== undefined && app.actions !== null) { for (let actionkey in app.actions) { const inneraction = app.actions[actionkey] if (inneraction.name.toLowerCase().includes(value)) { return true; } } } return false; }) } setVisibleApps(newApps) } else { setVisibleApps( prioritizedApps.concat( filteredApps.filter( (innerapp) => !internalIds.includes(innerapp.id.toLowerCase()) ) ) ) } }; const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => { if (document !== undefined) { const appsearchValue = document.getElementById("appsearch") if (appsearchValue !== undefined && appsearchValue !== null) { if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { refine(appsearchValue.value) } } } return (