diff --git a/README.md b/README.md index 3e22c1d4..c1a214e2 100755 --- a/README.md +++ b/README.md @@ -11,11 +11,13 @@ Shuffle Automation [Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be. +[ Get training ](https://shuffler.io/training) [_Key Features_](https://shuffler.io/docs/features) — [_Community & Support_](https://discord.gg/B2CBzUm) — [_Documentation_](https://shuffler.io/docs) — [_Getting Started_](https://shuffler.io/docs/getting_started) — [_Development_](https://github.com/shuffle/Shuffle/blob/master/.github/CONTRIBUTING.md) +[ Set up a demo call ](https://shuffler.io/contact) Follow us on Twitter at [@shuffleio](https://twitter.com/shuffleio). diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index a7e0d4f8..415f1e94 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -97,6 +97,7 @@ def md5_base64(a): a = str(a) foundhash = hashlib.md5(a.encode('utf-8')).hexdigest() return base64.b64encode(foundhash.encode('utf-8')) + @shuffle_filters.register def base64_encode(a): @@ -107,6 +108,17 @@ def base64_encode(a): except: return base64.b64encode(a).decode() +@shuffle_filters.register +def random_element(a): + # Choose a random item from an array + a = list(a) + + if len(a) == 0: + return "" + + return random.choice(a) + + @shuffle_filters.register def base64_decode(a): a = str(a) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 659f6364..34bb7658 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -20,15 +20,15 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.6.40 + github.com/shuffle/shuffle-shared v0.6.50 golang.org/x/crypto v0.22.0 google.golang.org/api v0.176.1 google.golang.org/grpc v1.63.2 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.30.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 + k8s.io/client-go v0.30.2 ) require ( @@ -58,7 +58,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/frikky/schemaless v0.0.11 // indirect + github.com/frikky/schemaless v0.0.13 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -100,6 +100,8 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sashabaranov/go-openai v1.19.2 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 4c0d4206..8bc3b6ce 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -160,6 +160,8 @@ github.com/frikky/schemaless v0.0.9 h1:RzNLPkJq5c4nlm5iLiTndFcbeQxdMGJIj266wSGt2 github.com/frikky/schemaless v0.0.9/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= github.com/frikky/schemaless v0.0.11 h1:c4r6CJX30XI+SoJdT9RlUd9qYSQlx6hvwGRtsypu+uM= github.com/frikky/schemaless v0.0.11/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= +github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ= +github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsouza/go-dockerclient v1.11.0 h1:4ZAk6W7rPAtPXm7198EFqA5S68rwnNQORxlOA5OurCA= @@ -303,6 +305,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -427,6 +431,10 @@ github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6 github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -443,6 +451,12 @@ github.com/shuffle/shuffle-shared v0.6.27 h1:q4qZD6bGZFIvZ5Y10unGr3N3rZ7OryWyvva github.com/shuffle/shuffle-shared v0.6.27/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= github.com/shuffle/shuffle-shared v0.6.31 h1:MK1SW1pwjIP7hznq+mMlTPM1R3LIOfi1/bUL5xFSg/8= github.com/shuffle/shuffle-shared v0.6.31/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= +github.com/shuffle/shuffle-shared v0.6.46 h1:v/IXc+4V8DCWflGKGgaI37uuGnsylMjqKRylwNoXVrA= +github.com/shuffle/shuffle-shared v0.6.46/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= +github.com/shuffle/shuffle-shared v0.6.47 h1:EOalfIIBX97uGkgRRPsdUApts6yCCcXf1iSphm6UoE0= +github.com/shuffle/shuffle-shared v0.6.47/go.mod h1:XVIcR2/GyIk+6qmlpOG3NvIao6kCOma36EdMym4AeSY= +github.com/shuffle/shuffle-shared v0.6.50 h1:MBeGAiBNkw9Eg+3YTJIlBOuskWntGvT0uefFUYOBhbY= +github.com/shuffle/shuffle-shared v0.6.50/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -977,10 +991,16 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index d51535fa..d24cbc64 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3616,7 +3616,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { } if org.SyncConfig.WorkflowBackup { - workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser) + workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") if err != nil { log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) } else { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 3d417472..7ff135e3 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1076,7 +1076,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request // return workflowExecution, fmt.Sprintf("%s", err), nil } else { log.Printf("[ERROR] Failed in prepareExecution: '%s'", err) - return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed starting workflow: %s", err), err + return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", err), err } } @@ -3382,6 +3382,8 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) { return } + debugUrl := fmt.Sprintf("/workflows/%s?execution_id=%s", workflowExecution.Workflow.ID, workflowExecution.ExecutionId) + resp.Header().Add("X-Debug-Url", debugUrl) workflowExecution.Priority = 11 environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id) diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index 0f3fedef..11e25f0b 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -232,6 +232,11 @@ const AppGrid = (props) => { removeQuery("q"); refine(event.currentTarget.value); }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} limit={5} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} @@ -994,6 +999,11 @@ const AppGrid = (props) => { onChange={(event) => { setSearchQuery(event.currentTarget.value); }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} limit={5} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index e52bdce7..26b88ca5 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -7,7 +7,7 @@ import countries from "../components/Countries.jsx"; import { Box, Paper, - Typography, + Typography, Divider, Button, Grid, @@ -19,94 +19,150 @@ import { DialogTitle, DialogContent, TextField, - InputAdornment, + InputAdornment, IconButton, - Chip, + Chip, Checkbox, - Tooltip, + Tooltip, + DialogContentText, + DialogActions, + LinearProgress } from "@mui/material"; -import { useNavigate, Link } from "react-router-dom"; +import { useNavigate, Link, json } from "react-router-dom"; import { Autocomplete } from "@mui/material"; -import { toast } from "react-toastify" +import { toast } from "react-toastify" import { - Cached as CachedIcon, - ContentCopy as ContentCopyIcon, - Draw as DrawIcon, - Close as CloseIcon, + Cached as CachedIcon, + ContentCopy as ContentCopyIcon, + Draw as DrawIcon, + Close as CloseIcon, + Delete, + RestaurantRounded, } from "@mui/icons-material"; //import { useAlert import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; import BillingStats from "../components/BillingStats.jsx"; import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" +import DeleteIcon from '@mui/icons-material/Delete'; const Billing = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props; - //const alert = useAlert(); - let navigate = useNavigate(); + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props; + //const alert = useAlert(); + let navigate = useNavigate(); - const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); - const [dealList, setDealList] = React.useState([]); - const [dealName, setDealName] = React.useState(""); - const [dealAddress, setDealAddress] = React.useState(""); - const [dealType, setDealType] = React.useState("MSSP"); - const [dealCountry, setDealCountry] = React.useState("United States"); - const [dealCurrency, setDealCurrency] = React.useState("USD"); - const [dealStatus, setDealStatus] = React.useState("initiated"); - const [dealValue, setDealValue] = React.useState(""); - const [dealDiscount, setDealDiscount] = React.useState(""); - const [dealerror, setDealerror] = React.useState(""); + const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); + const [dealList, setDealList] = React.useState([]); + const [dealName, setDealName] = React.useState(""); + const [dealAddress, setDealAddress] = React.useState(""); + const [dealType, setDealType] = React.useState("MSSP"); + const [dealCountry, setDealCountry] = React.useState("United States"); + const [dealCurrency, setDealCurrency] = React.useState("USD"); + const [dealStatus, setDealStatus] = React.useState("initiated"); + const [dealValue, setDealValue] = React.useState(""); + const [dealDiscount, setDealDiscount] = React.useState(""); + const [dealerror, setDealerror] = React.useState(""); + const [openChangeEmailBox, setOpenChangeEmailBox] = useState(false); + const [isMouseOverOnChangeEmail, setIsMouseOverOnChangeEmail] = useState(false); + const [currentAppRunsInPercentage, setCurrentAppRunsInPercentage] = useState(0); + const [currentAppRunsInNumber, setCurrentAppRunsInNumber] = useState(0); + const [alertThresholds, setAlertThresholds] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.AlertThreshold !== undefined && selectedOrganization.Billing.AlertThreshold !== null ? selectedOrganization.Billing.AlertThreshold : [{ percentage: '', count: '', Email_send: false }]); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { + const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100; + setCurrentAppRunsInPercentage(Math.round(percentage)); + setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage); + } + }, [userdata]); + + + const [BillingEmail, setBillingEmail] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.Email !== undefined && selectedOrganization.Billing.Email != null && selectedOrganization.Billing.Email.length > 0 ? selectedOrganization.Billing.Email : selectedOrganization.org); + + useState(() => { + // Set the billing email + setBillingEmail( + selectedOrganization.Billing !== undefined && + selectedOrganization.Billing.Email !== undefined && + selectedOrganization.Billing.Email.length > 0 + ? selectedOrganization.Billing.Email + : selectedOrganization.org + ); + + // Set and sort the alert thresholds + const alertThresholds = selectedOrganization.Billing !== undefined && + selectedOrganization.Billing.AlertThreshold !== undefined && + selectedOrganization.Billing.AlertThreshold !== null + ? selectedOrganization.Billing.AlertThreshold + : [{ percentage: '', count: '', Email_send: false }]; + + const sortedAlertThresholds = alertThresholds.sort((a, b) => { + const countA = parseFloat(a.count); + const countB = parseFloat(b.count); + if (isNaN(countA)) return 1; + if (isNaN(countB)) return -1; + + return countA - countB; + }); + + setAlertThresholds(sortedAlertThresholds); + + const findCurrentIndex = sortedAlertThresholds.some(threshold => threshold.Email_send === false); + setCurrentIndex(findCurrentIndex ? sortedAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1); + + }, [selectedOrganization]); const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : "" const products = [ - { code: "", label: "MSSP", phone: "" }, - { code: "", label: "Enterprise", phone: "" }, - { code: "", label: "Consultancy", phone: "" }, - { code: "", label: "Support", phone: "" }, - ]; + { code: "", label: "MSSP", phone: "" }, + { code: "", label: "Enterprise", phone: "" }, + { code: "", label: "Consultancy", phone: "" }, + { code: "", label: "Support", phone: "" }, + ]; const handleGetDeals = (orgId) => { - console.log("Get deals!"); + console.log("Get deals!"); - if (orgId.length === 0) { - toast( - "Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout." - ); - return; - } + if (orgId.length === 0) { + toast( + "Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout." + ); + return; + } - const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; - fetch(url, { - method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => { - if (response.status !== 200) { - console.log("Bad status code in get deals: ", response.status); - } + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Bad status code in get deals: ", response.status); + } - return response.json(); - }) - .then((responseJson) => { - console.log("Got deals: ", responseJson); - if (responseJson.success === false) { - toast("Failed loading deals. Contact support if this persists"); - } else { - setDealList(responseJson); - } - }) - .catch((error) => { - console.log("Error getting org deals: ", error); - toast( - "Failed getting deals for your org. Contact support if this persists." - ); - }); - }; + return response.json(); + }) + .then((responseJson) => { + console.log("Got deals: ", responseJson); + if (responseJson.success === false) { + toast("Failed loading deals. Contact support if this persists"); + } else { + setDealList(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org deals: ", error); + toast( + "Failed getting deals for your org. Contact support if this persists." + ); + }); + }; useEffect(() => { if (isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true) { @@ -121,15 +177,15 @@ const Billing = (props) => { maxWidth: 400, width: "100%", backgroundColor: theme.palette.platformColor, - borderRadius: theme.palette.borderRadius*2, + borderRadius: theme.palette.borderRadius * 2, border: "1px solid rgba(255,255,255,0.3)", - marginRight: 10, + marginRight: 10, marginTop: 15, } - const isCloud = - window.location.host === "localhost:3002" || - window.location.host === "shuffler.io"; + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; billingInfo.subscription = { "active": true, @@ -161,82 +217,83 @@ const Billing = (props) => { var checkoutObject = { lineItems: [ { - price: priceItem, + price: priceItem, quantity: 1 }, ], mode: "subscription", billingAddressCollection: "auto", - successUrl: successUrl, - cancelUrl: failUrl, + successUrl: successUrl, + cancelUrl: failUrl, clientReferenceId: props.userdata.active_org.id, } //submitType: "donate", stripe.redirectToCheckout(checkoutObject) - .then(function (result) { - console.log("SUCCESS STRIPE?: ", result) + .then(function (result) { + console.log("SUCCESS STRIPE?: ", result) - ReactGA.event({ - category: "pricing", - action: "add_card_success", - label: "", + ReactGA.event({ + category: "pricing", + action: "add_card_success", + label: "", + }) }) - }) - .catch(function(error) { - console.error("STRIPE ERROR: ", error) + .catch(function (error) { + console.error("STRIPE ERROR: ", error) - ReactGA.event({ - category: "pricing", - action: "add_card_error", - label: "", - }) - }); + ReactGA.event({ + category: "pricing", + action: "add_card_error", + label: "", + }) + }); } const cancelSubscriptions = (subscription_id) => { - const orgId = selectedOrganization.id; - const data = { - subscription_id: subscription_id, - action: "cancel", - org_id: selectedOrganization.id, - }; + const orgId = selectedOrganization.id; + const data = { + subscription_id: subscription_id, + action: "cancel", + org_id: selectedOrganization.id, + }; - const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } - - if (handleGetOrg != undefined) { - handleGetOrg(selectedOrganization.id); + const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success !== undefined && responseJson.success) { - toast("Successfully stopped subscription!"); - } else { - toast("Failed stopping subscription. Please contact us."); - } - }) - .catch(function (error) { - console.log("Error: ", error); - toast("Failed stopping subscription. Please contact us."); - }); - }; + if (handleGetOrg != undefined) { + handleGetOrg(selectedOrganization.id); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success !== undefined && responseJson.success) { + toast("Successfully stopped subscription!"); + } else { + toast("Failed stopping subscription. Please contact us."); + } + }) + .catch(function (error) { + console.log("Error: ", error); + toast("Failed stopping subscription. Please contact us."); + }); + }; + const sendSignatureRequest = (subscription) => { const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; @@ -246,46 +303,47 @@ const Billing = (props) => { org_id: selectedOrganization.id, subscription: subscription, }), - mode: "cors", - method: "POST", - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => { - if (response.status !== 200) { - console.log("Error in response"); - } - return response.json(); - }) - .then((responseJson) => { - console.log("Response from signature request: ", responseJson); - }) - .catch((error) => { - console.log("Error: ", error); + mode: "cors", + method: "POST", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, }) + .then((response) => { + if (response.status !== 200) { + console.log("Error in response"); + } + return response.json(); + }) + .then((responseJson) => { + console.log("Response from signature request: ", responseJson); + }) + .catch((error) => { + console.log("Error: ", error); + }) } const SubscriptionObject = (props) => { - const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; + const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; - const [signatureOpen, setSignatureOpen] = React.useState(false); - const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) + const [signatureOpen, setSignatureOpen] = React.useState(false); + const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) const [hovered, setHovered] = React.useState(false) + const [newBillingEmail, setNewBillingEmail] = useState(''); var top_text = "Base Cloud Access" if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { subscription.name = "Enterprise" subscription.currency_text = "$" - subscription.price = subscription.level*180 - subscription.limit = subscription.level*100000 + subscription.price = subscription.level * 180 + subscription.limit = subscription.level * 100000 subscription.interval = subscription.recurrence subscription.features = [ "Includes " + subscription.limit + " app runs/month. ", - "Multi-Tenancy and Region-Selection", + "Multi-Tenancy and Region-Selection", "And all other features from /pricing", ] } @@ -301,17 +359,17 @@ const Billing = (props) => { if (subscription.name.includes("default")) { top_text = "Custom Contract" newPaperstyle.border = "1px solid #f85a3e" - showSupport = true + showSupport = true } if (subscription.name.includes("App Run Units")) { top_text = "Cloud Access" - showSupport = true + showSupport = true } if (subscription.name.includes("Open Source")) { top_text = "Open Source" - showSupport = true + showSupport = true } if (subscription.name.includes("Scale")) { @@ -327,8 +385,80 @@ const Billing = (props) => { newPaperstyle.backgroundColor = theme.palette.surfaceColor } + const handleClickOpen = () => { + setOpenChangeEmailBox(true); + }; + + const handleCloseChangeEmailBox = () => { + setOpenChangeEmailBox(false); + }; + + + const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key, value) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; + }; + + const HandleChangeBillingEmail = (orgId) => { + const email = newBillingEmail; + const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; + console.log("Pattern matches: ", emailPattern.test(email)); + if (!emailPattern.test(email)) { + toast("Please enter a valid email address"); + return; + } else { + setNewBillingEmail(email); + } + + toast("Updating billing email. Please Wait") + + const data = { + org_id: orgId, + email: newBillingEmail, + billing: { + email: newBillingEmail, + }, + }; + + const url = `${globalUrl}/api/v1/orgs/${orgId}/billing`; + fetch(url, { + method: "POST", + body: JSON.stringify(data), + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Bad status code in get org:", response.status); + } + return response.json(); + }).then((responseJson) => { + console.log("Got org:", responseJson); + if (responseJson.success === true) { + toast.success("Successfully updated billing email"); + setBillingEmail(newBillingEmail); + setOpenChangeEmailBox(false); + } else { + toast.error("Failed to update billing email. Please try again."); + } + }) + .catch((error) => { + console.log("Error getting org:", error); + }); + } + return ( - setHovered(true)} onMouseLeave={() => setHovered(false)} @@ -336,34 +466,34 @@ const Billing = (props) => { - { - e.preventDefault(); - setSignatureOpen(false); - setTosChecked(false) - }} - > - - + { + e.preventDefault(); + setSignatureOpen(false); + setTosChecked(false) + }} + > + + Read and Accept the EULA @@ -371,13 +501,13 @@ const Billing = (props) => { rows={17} multiline fullWidth - InputProps={{ - readOnly: true, - style: { - fontSize: 14, - color: "rgba(255, 255, 255, 0.6)", - } - }} + InputProps={{ + readOnly: true, + style: { + fontSize: 14, + color: "rgba(255, 255, 255, 0.6)", + } + }} value={subscription.eula} /> { }} inputProps={{ 'aria-label': 'primary checkbox' }} /> - { + { setTosChecked(!tosChecked) }}> Accept - + By clicking the “accept” button, you are signing the document, electronically agreeing that it has the same legal validity and effects as a handwritten signature, and that you have the competent authority to represent and sign on behalf an entity. Need support or have questions? Contact us at support@shuffler.io. -
+
-
+
{top_text === "Base Cloud Access" && userdata.has_card_available === true ? { @@ -436,80 +566,80 @@ const Billing = (props) => { }} variant="outlined" color="primary" - /> + /> : null} {top_text} {top_text === "Base Cloud Access" && userdata.has_card_available === false ? - - : null} + : null} {isCloud && highlight === true && top_text !== "Base Cloud Access" ? - { setSignatureOpen(true) }} > - + - : null} + : null}
- -
- - {subscription.name} - + +
+ + {subscription.name} + - {subscription.currency_text !== undefined ? -
- - {subscription.currency_text}{subscription.price} - - - / {subscription.interval} - -
+ {subscription.currency_text !== undefined ? +
+ + {subscription.currency_text}{subscription.price} + + + / {subscription.interval} + +
: null} - - Features - -
    + + Features + +
      {subscription.features !== undefined && subscription.features !== null ? subscription.features.map((feature, index) => { var parsedFeature = feature if (feature.includes("Documentation: ")) { - parsedFeature = - Documentation to get started } if (feature.includes("Worker License: ")) { - const fieldId = "webhook_uri_field_"+index + const fieldId = "webhook_uri_field_" + index parsedFeature = - + @@ -517,47 +647,47 @@ const Billing = (props) => { {}} - InputProps={{ - endAdornment: - - { - var copyText = document.getElementById(fieldId); - if (copyText !== undefined && copyText !== null) { - console.log("NAVIGATOR: ", navigator); - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } + style={{ + backgroundColor: theme.palette.inputColor, + borderRadius: theme.palette.borderRadius, + }} + id={fieldId} + onClick={() => { }} + InputProps={{ + endAdornment: + + { + var copyText = document.getElementById(fieldId); + if (copyText !== undefined && copyText !== null) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } - navigator.clipboard.writeText(copyText.value); - copyText.select(); - copyText.setSelectionRange( - 0, - 99999 - ); /* For mobile devices */ + navigator.clipboard.writeText(copyText.value); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ - /* Copy the text inside the text field */ - document.execCommand("copy"); - toast("Copied Webhook URL"); - } else { - console.log("Couldn't find webhook URI field: ", copyText); - } - }} - edge="end" - > - - - - }} + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied Webhook URL"); + } else { + console.log("Couldn't find webhook URI field: ", copyText); + } + }} + edge="end" + > + + + + }} fullWidth /> @@ -565,30 +695,105 @@ const Billing = (props) => { return (
    • - + {parsedFeature}
    • ) }) : null} -
    -
- {isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? - - - {subscription.name.includes("Scale") ? - "" - : + +
+ {isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? + + + {subscription.name.includes("Scale") ? + "" + : - userdata.has_card_available === true ? - "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." - : + userdata.has_card_available === true ? + "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." + : + isCloud ? `You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.` - } + : + `You are not subscribed to any plan and are using the free, open source plan. This plan has no enforced limits, but scale issues may occur due to CPU congestion.` + } + +
+ + Billing email: {BillingEmail} - Billing email: {selectedOrganization.org} - {/*isCloud ? + {userdata.has_card_available === true && ( + + )} + + Change Billing Email + + + Enter the new billing email address. + + { if (event.key === 'Enter') HandleChangeBillingEmail(selectedOrganization.id) }} + onChange={(e) => setNewBillingEmail(e.target.value)} + /> + + + + + + +
+ {/*isCloud ? : null*/} - + {userdata.has_card_available === true ? + - {userdata.has_card_available === true ? - : null} - -
+ + : null} - {showSupport ? + {showSupport ? - : null } - + : null} + ) } const addDealModal = ( - { - setSelectedDealModalOpen(false); - }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - - Register new deal - - -
- { - setDealName(e.target.value); - }} - /> - { - setDealAddress(e.target.value); - }} - /> -
-
- { - setDealValue(e.target.value); - }} - /> - option.label} - onChange={(event, newValue) => { - setDealCountry(newValue.label); - }} - renderOption={(props, option) => ( - img": { mr: 2, flexShrink: 0 } }} - {...props} - > - - {option.label} ({option.code}) +{option.phone} - - )} - renderInput={(params) => ( - - )} - /> - { - setDealType(newValue); - }} - getOptionLabel={(option) => option.label} - renderOption={(props, option) => ( - img": { mr: 2, flexShrink: 0 } }} - {...props} - > - {option.label} - - )} - renderInput={(params) => ( - - )} - /> -
- {dealerror.length > 0 ? ( - - error registering: {dealerror} - - ) : null} -
- - -
-
-
- ); + //setDealName("") + //setDealAddress("") + //setDealCountry("") + //setDealValue("") + }} + > + Cancel + + +
+ + + ); - const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => { - if (dealerror.length > 0) { - setDealerror(""); - } + const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => { + if (dealerror.length > 0) { + setDealerror(""); + } - const orgId = selectedOrganization.id; - const data = { - reseller_org: orgId, - name: dealName, - address: dealAddress, - country: dealCountry, - value: dealValue, - }; + const orgId = selectedOrganization.id; + const data = { + reseller_org: orgId, + name: dealName, + address: dealAddress, + country: dealCountry, + value: dealValue, + }; - const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - setSelectedDealModalOpen(false); - toast( - "Added new deal! We will be in touch shortly with an update." - ); + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + setSelectedDealModalOpen(false); + toast( + "Added new deal! We will be in touch shortly with an update." + ); + + setDealName(""); + setDealAddress(""); + setDealValue(""); + setDealCountry("United States"); + setDealType("MSSP"); + } else { + setDealerror(responseJson.reason); + } + }) + .catch(function (error) { + //console.log("Error: ", error); + setDealerror(error.toString()); + toast("Failed adding deal reg: ", error); + }); + }; + const addAlertThreshold = () => { + setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]); + }; + + const updateAlertThreshold = (index, field, value) => { + + if (field === 'percentage') { + if (value > 100 || value < 0) { + value = 0 + toast("The percentage value should be between 0 and 100") + } + } else if (field === 'count') { + if (value < 0 || value >= userdata.app_execution_limit) { + value = 0 + toast("The count value should be greater than 0 and less than the total app execution limit") + } + } + + + const totalValue = userdata.app_execution_limit; + const newAlertThresholds = alertThresholds.map((threshold, i) => { + if (i === index) { + const newValue = parseFloat(value); + if (field === 'percentage') { + const newCount = (newValue / 100) * totalValue; + return { + ...threshold, + percentage: isNaN(newValue) ? '' : Math.round(newValue), + count: isNaN(newCount) ? '' : Math.round(newCount), + Email_send: false + }; + } else if (field === 'count') { + const newPercentage = (newValue / totalValue) * 100; + return { + ...threshold, + count: newValue, + percentage: isNaN(newPercentage) ? '' : Math.round(newPercentage), + Email_send: false + }; + } + } + return threshold; + }); + setAlertThresholds(newAlertThresholds); + }; + + + const handleDeleteAlertThreshold = (index) => { + const newAlertThresholds = alertThresholds.filter((_, i) => i !== index); + setAlertThresholds(newAlertThresholds); + + // Update currentIndex based on remaining elements + const findCurrentIndex = newAlertThresholds.some(threshold => threshold.Email_send === false); + setCurrentIndex(findCurrentIndex ? newAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1); + }; + + const HandleEditOrgForAlertThreshold = (orgId) => { + + // Use the `some` method to check for invalid counts + const invalidCount = alertThresholds.some((threshold) => { + if (threshold.count === '') { + toast("Please enter a valid Count or Percentage value"); + return true; // Stop checking further and return true if invalid + } + return false; + }); + + // If any invalid count is found, return early + if (invalidCount) { + return; + } + + toast("Updating Email Alert Threshold. Please wait..."); + + const data = { + org_id: orgId, + billing: { + email: BillingEmail, + AlertThreshold: alertThresholds.map(threshold => ({ + ...threshold, + percentage: parseInt(threshold.percentage, 10), + count: parseInt(threshold.count, 10), + })), + }, + }; + + const url = `${globalUrl}/api/v1/orgs/${orgId}`; + fetch(url, { + method: "POST", + body: JSON.stringify(data), + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Bad status code in get org:", response.status); + } + return response.json(); + }).then((responseJson) => { + console.log("Got org:", responseJson); + if (responseJson.success === true) { + toast.success("Successfully updated Email Alert Thresholds"); + const findCurrentIndex = alertThresholds.some(threshold => threshold.Email_send === false); + setCurrentIndex(findCurrentIndex ? alertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1); + } else { + toast.error("Failed to update Email Alert Thresholds. Please try again."); + } + }) + .catch((error) => { + console.log("Error getting org:", error); + }); + }; + + const getSafeValue = (value) => { + + if (value === undefined || value === null || isNaN(value)) { + return 0; + } else { + return value; + } + }; - setDealName(""); - setDealAddress(""); - setDealValue(""); - setDealCountry("United States"); - setDealType("MSSP"); - } else { - setDealerror(responseJson.reason); - } - }) - .catch(function (error) { - //console.log("Error: ", error); - setDealerror(error.toString()); - toast("Failed adding deal reg: ", error); - }); - }; const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null return ( -
- {addDealModal} - {clickedFromOrgTab? -

Billing & Licensing

: - - Billing & Licensing - } - {clickedFromOrgTab? - {isCloud ? - "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." - : - "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." - }: - - {isCloud ? +
+ {addDealModal} + {clickedFromOrgTab ? +

Billing & Licensing

: + + Billing & Licensing + } + {clickedFromOrgTab ? + {isCloud ? "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." : "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." - } - } + } : + + {isCloud ? + "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." + : + "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." + } + } - {userdata.support === true ? -
+ {userdata.support === true ? +
For sales: Create  New Cloud Contract @@ -1001,7 +1330,7 @@ const Billing = (props) => { New Onprem Contract -   -   +   -   Google Drive Link @@ -1009,22 +1338,20 @@ const Billing = (props) => { Sales Process - -
: null } - {isChildOrg ? - - Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this. - + {isChildOrg ? + + Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this. + : null} -
- {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : +
+ {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : { subscription={billingInfo.subscription} highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0} /> - : !isCloud ? - - - - - : null} + : !isCloud ? + + + + + : null} {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 && - !isChildOrg ? - selectedOrganization.subscriptions - .reverse() - .map((sub, index) => { - return ( - - ) - }) - : null} - {/* + !isChildOrg ? + selectedOrganization.subscriptions + .reverse() + .map((sub, index) => { + return ( + + ) + }) + : null} + {/* { */} -
+
- {/*isCloud && + {/*isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true ? (
@@ -1333,21 +1660,156 @@ const Billing = (props) => {
) : null*/} -
+
- Utilization & Stats + Manage Billing -
- + Manage your billing and licensing information below. When you reach the certain thresholds of your subscription limit, you will be notified by email. + + Current Usage: + + + You have used {currentAppRunsInPercentage}% of total app execution limit or {userdata.app_execution_usage} app runs out of {userdata.app_execution_limit} app runs. + + +
+ + Set email alert thresholds for app runs + + + You will be notified by email when you reach the + {currentIndex !== -1 + ? " " + getSafeValue(alertThresholds[currentIndex].percentage) + '%' + " " + : " " + '0%' + " " + } + of your total app execution limit or + {currentIndex !== -1 + ? " " + getSafeValue(alertThresholds[currentIndex].count) + " " + : " " + 0 + " "} + app runs. + + +
+ {alertThresholds.map((threshold, index) => ( +
+ updateAlertThreshold(index, 'percentage', e.target.value)} + margin="normal" + variant="outlined" + inputProps={{ + max: 100, + }} + /> + updateAlertThreshold(index, 'count', e.target.value)} + margin="normal" + variant="outlined" + /> + { + alertThresholds.length > 1 && + ( + + ) + } +
+ ))} +
+
+ + + +
+
+ + Utilization & Stats + +
+ + />
) } diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index ac18d39b..519c7470 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -134,6 +134,9 @@ const AppStats = (defaultprops) => { Accept: "application/json", }, credentials: "include", + }).catch((error) => { + console.log("Error getting workflow stats: " + error); + return workflow }) if (response.status !== 200) { diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index 6a7b55b7..6c8ae0ee 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -428,7 +428,6 @@ const ConfigureWorkflow = (props) => { console.log("Found webhook: ", trigger) if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") { - console.log("Actions: ", newactions) const findapp = trigger.app_association.name.toLowerCase() const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp) @@ -449,9 +448,6 @@ const ConfigureWorkflow = (props) => { newactions[foundindex].show_steps = true - console.log("CHANGED ACTION: ", newactions[foundindex]) - //console.log("Index: ", newactions[foundindex]) - continue } } @@ -1321,7 +1317,6 @@ const ConfigureWorkflow = (props) => { } if (step.type === "authenticate") { - console.log("AUTH STEP: ", step) if (data.must_authenticate === true ) { filled = false } else { diff --git a/frontend/src/components/CreatorGrid.jsx b/frontend/src/components/CreatorGrid.jsx index ce6a197f..f3922e3e 100644 --- a/frontend/src/components/CreatorGrid.jsx +++ b/frontend/src/components/CreatorGrid.jsx @@ -136,6 +136,11 @@ const CreatorGrid = props => { removeQuery("q") refine(event.currentTarget.value) }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} @@ -147,6 +152,7 @@ const CreatorGrid = props => { flexWrap: "wrap", alignContent: "space-between", marginTop: 5, + padding:"0px 180px", } const Hits = ({ hits }) => { diff --git a/frontend/src/components/DiscordChat.jsx b/frontend/src/components/DiscordChat.jsx index e1d226b8..5f83c119 100644 --- a/frontend/src/components/DiscordChat.jsx +++ b/frontend/src/components/DiscordChat.jsx @@ -75,7 +75,12 @@ const DiscordChat = props => { fullWidth value={currentRefinement} onChange={(event) => refine(event.currentTarget.value)} - placeholder="Search Discord Chats" + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} + placeholder="Search Discord Chats..." style={{ backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%", }} InputProps={{ style: { @@ -143,7 +148,7 @@ const DiscordChat = props => { const CustomHits = connectHits(Hits); return ( -
+
diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx index 06469251..7a32001c 100644 --- a/frontend/src/components/DocsGrid.jsx +++ b/frontend/src/components/DocsGrid.jsx @@ -124,6 +124,11 @@ const DocsGrid = props => { removeQuery("q") refine(event.currentTarget.value) }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} limit={5} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} @@ -274,7 +279,7 @@ const DocsGrid = props => {
*/} -
+
diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 9ac7fabe..e2443664 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -78,7 +78,6 @@ const EditWorkflow = (props) => { const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day')) - console.log("WORKFLOW: ", workflow) const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const classes = useStyles(); @@ -235,7 +234,7 @@ const EditWorkflow = (props) => {
-
+
{/*
@@ -339,26 +338,6 @@ const EditWorkflow = (props) => { autoFocus fullWidth /> -
- { - setDescription(event.target.value) - }} - InputProps={{ - style: { - color: "white", - }, - }} - maxRows={4} - color="primary" - defaultValue={innerWorkflow.description} - placeholder="Description" - multiline - label="Description" - margin="dense" - fullWidth - /> -
{usecases !== null && usecases !== undefined && usecases.length > 0 ? @@ -426,27 +405,56 @@ const EditWorkflow = (props) => { fullWidth value={newWorkflowTags} onChange={(chip) => { - console.log("Chip: ", chip) - //newWorkflowTags.push(chip); setNewWorkflowTags(chip); }} + onBlur={(event) => { + if (event.target.value.length === 0) { + return + } + + if (newWorkflowTags.includes(event.target.value)) { + return + } + + newWorkflowTags.push(event.target.value) + setNewWorkflowTags(newWorkflowTags) + + setUpdate(Math.random()) + }} onAdd={(chip) => { - newWorkflowTags.push(chip); - setNewWorkflowTags(newWorkflowTags); + newWorkflowTags.push(chip) + setNewWorkflowTags(newWorkflowTags) }} onDelete={(chip, index) => { console.log("Deleting: ", chip, index) - newWorkflowTags.splice(index, 1); - setNewWorkflowTags(newWorkflowTags); - setUpdate(Math.random()); + newWorkflowTags.splice(index, 1) + setNewWorkflowTags(newWorkflowTags) + setUpdate(Math.random()) }} />
{showMoreClicked === true ? - - - +
+ { + setDescription(event.target.value) + }} + InputProps={{ + style: { + color: "white", + }, + }} + multiLine + rows={3} + color="primary" + defaultValue={innerWorkflow.description} + placeholder="Description" + multiline + label="Description" + margin="dense" + fullWidth + />
@@ -565,14 +573,24 @@ const EditWorkflow = (props) => { fullWidth /> - - MSSP Suborg Distribution (beta - contact support@shuffler.io) + + + + MSSP Suborg Distribution (beta - contact support@shuffler.io for more info) {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? - - You can only distribute to suborgs from a parent org. - + userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? + + Your organization does not have any suborgs yet. Please make one, then try again. + + : + + {innerWorkflow.parentorg_workflow !== undefined && innerWorkflow.parentorg_workflow !== null && innerWorkflow.parentorg_workflow.length > 0 ? This workflow is distributed from your parent workflow (you may not have access). : null} +
+
+ You can only distribute to suborgs from a parent org. +
: { maxWidth: 150, float: "right", }} - value={selectedNamespace} + value={selectedCategory} onChange={(event) => { - setSelectedNamespace(event.target.value); + setSelectedCategory(event.target.value) if (event.target.value === "all" || event.target.value === "default") { getFiles() } else { getFiles(event.target.value) } + + // Add it to the url as a query + if (window.location.search.includes("category=")) { + const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`) + window.history.pushState({ path: newurl }, "", newurl) + } else { + window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`) + } }} > - {fileNamespaces.map((data, index) => { + {fileCategories.map((data, index) => { return ( {
{renderTextBox ? - - - - : - - - } - + + + + : + + + + } {renderTextBox && { @@ -783,6 +814,7 @@ const Files = (props) => { isFileEditor = {true} key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children runUpdateText = {runUpdateText} + contentLoading = {contentLoading} /> {isSelectedFiles?null: { + {/* + */} { /> { file.namespace = "default"; } - if (file.namespace !== selectedNamespace) { + if (file.namespace !== selectedCategory) { return null; } @@ -853,6 +887,7 @@ const Files = (props) => { overflow: "hidden", }} > + {/* { }} primary={new Date(file.updated_at * 1000).toISOString()} /> + */} { { disabled={file.status !== "active"} style = {{padding: "6px"}} onClick={() => { - deleteFile(file); + deleteFile(file) }} > { setAnchorEl(event.currentTarget); }; + useEffect(() => { + const topbar = localStorage.getItem("topbar_closed") + if (topbar === "true") { + setShowTopbar(false) + } else { + setShowTopbar(true) + } + }, []) + const handleClose = () => { setAnchorEl(null); setAnchorElAvatar(null); @@ -537,7 +546,7 @@ const Header = (props) => { rel="noopener noreferrer" target="_blank" > - + { aria-haspopup="true" onClick={(event) => { }} > - Discord Community Join + + {/*#f865f2*/} @@ -1590,17 +1596,17 @@ const Header = (props) => { navigate("/training") - //if (window.drift !== undefined) { - // window.drift.api.startInteraction({ interactionId: 341911 }) - //} else { - // console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) - //} }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> Public Training! - { setShowTopbar(false) }}> + { + setShowTopbar(false) + + // Set storage that it's clicked + localStorage.setItem("topbar_closed", "true") + }}>
diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index 1ebf81dd..e9e794b2 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -471,32 +471,26 @@ const AuthenticationOauth2 = (props) => { state += `%26refresh_uri%3d${authentication_url}`; } - // No prompt forcing - //var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`; + // FIXME: Should this be =consent? var defaultPrompt = "login" if (prompt !== undefined && prompt !== null && prompt.length > 0) { - defaultPrompt = prompt - } + defaultPrompt = prompt + } - var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`; + var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`; + if (admin_consent === true) { + console.log("Running Oauth2 WITH admin consent") + //url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`; + url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`; + } - if (admin_consent === true) { - console.log("Running Oauth2 WITH admin consent") - //url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`; - url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`; - } + console.log("OAUTH2 URL: ", url) - // Force new consent + // Force new consent //const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`; - // Admin consent + // Admin consent //const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent` - - // &resource=https%3A%2F%2Fgraph.microsoft.com& - - // FIXME: Awful, but works for prototyping - // How can we get a callback properly realtime? - // How can we properly try-catch without breaks on error? try { var newwin = window.open(url, "", "width=582,height=700"); //console.log(newwin) @@ -997,7 +991,6 @@ const AuthenticationOauth2 = (props) => { color: "white", padding: 5, minWidth: 300, - maxWidth: 300, }} onChange={(e, value) => { //handleScopeChange(e) diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 2293e641..4cfbd690 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -1,710 +1,549 @@ -import React, { useEffect } from "react"; +import React, { useEffect } from "react"; + +import { makeStyles } from "@mui/styles"; +import theme from '../theme.jsx'; +import { toast } from "react-toastify" +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import SubflowSuggestions from "../components/SubflowSuggestions.jsx"; + +import { + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Typography, + Switch, + Select, + MenuItem, + Divider, + TextField, + Button, + Tabs, + Tab, + Grid, + IconButton, + Autocomplete, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Box +} from "@mui/material"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, +} from "@mui/icons-material"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}) + +const OrgHeaderexpanded = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, -import { makeStyles } from "@mui/styles"; -import theme from '../theme.jsx'; -import { toast } from "react-toastify" -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import SubflowSuggestions from "../components/SubflowSuggestions.jsx"; + selectedStatus, + setSelectedStatus, + isEditOrgTab + } = props; + + const classes = useStyles(); + const defaultBranch = "master"; + + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + const [appDownloadUrl, setAppDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo === undefined || + selectedOrganization.defaults.app_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo + ); + const [appDownloadBranch, setAppDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.app_download_branch === undefined || + selectedOrganization.defaults.app_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.app_download_branch + ); + const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.workflow_download_repo === undefined || + selectedOrganization.defaults.workflow_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-workflows" + : selectedOrganization.defaults.workflow_download_repo + ); + const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch === undefined || + selectedOrganization.defaults.workflow_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch + ); + const [ssoEntrypoint, setSsoEntrypoint] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_entrypoint === undefined || + selectedOrganization.sso_config.sso_entrypoint.length === 0 + ? "" + : selectedOrganization.sso_config.sso_entrypoint + ); + const [ssoCertificate, setSsoCertificate] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_certificate === undefined || + selectedOrganization.sso_config.sso_certificate.length === 0 + ? "" + : selectedOrganization.sso_config.sso_certificate + ); + const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined + ? false + : selectedOrganization.sso_config.SSORequired === undefined + ? false + : selectedOrganization.sso_config.SSORequired); + + const [notificationWorkflow, setNotificationWorkflow] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.notification_workflow === undefined || + selectedOrganization.defaults.notification_workflow.length === 0 + ? "" + : selectedOrganization.defaults.notification_workflow + ); + + const [documentationReference, setDocumentationReference] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.documentation_reference === undefined || + selectedOrganization.defaults.documentation_reference.length === 0 + ? "" + : selectedOrganization.defaults.documentation_reference + ); + const [openidClientId, setOpenidClientId] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_id === undefined || + selectedOrganization.sso_config.client_id.length === 0 + ? "" + : selectedOrganization.sso_config.client_id + ); + const [openidClientSecret, setOpenidClientSecret] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_secret === undefined || + selectedOrganization.sso_config.client_secret.length === 0 + ? "" + : selectedOrganization.sso_config.client_secret + ); + const [openidAuthorization, setOpenidAuthorization] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_authorization === undefined || + selectedOrganization.sso_config.openid_authorization.length === 0 + ? "" + : selectedOrganization.sso_config.openid_authorization + ); + const [openidToken, setOpenidToken] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_token === undefined || + selectedOrganization.sso_config.openid_token.length === 0 + ? "" + : selectedOrganization.sso_config.openid_token + ) -import { - FormControl, - InputLabel, - Paper, - OutlinedInput, - Checkbox, - Card, - Tooltip, - FormControlLabel, - Typography, - Switch, - Select, - MenuItem, - Divider, - TextField, - Button, - Tabs, - Tab, - Grid, - IconButton, - Autocomplete, - Dialog, - DialogTitle, - DialogActions, - DialogContent, - Box -} from "@mui/material"; + const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo) + const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch) + const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username) + const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token) + + const [workflows, setWorkflows] = React.useState([]) + const [workflow, setWorkflow] = React.useState({}) + + const getAvailableWorkflows = (trigger_index) => { + fetch(globalUrl + "/api/v1/workflows", { + 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) { + setWorkflows(responseJson) + + if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { + + const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) + if (workflow !== undefined && workflow !== null) { + setWorkflow(workflow) + } + } + } + }) + .catch((error) => { + console.log("Error getting workflows: " + error); + }) + } + + useEffect(() => { + getAvailableWorkflows() + }, []) + + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config + ) => { + + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + }; + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + + const handleWorkflowSelectionUpdate = (e, isUserinput) => { + if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { + console.log("Returning as there's no id") + return null + } + + setWorkflow(e.target.value) + setNotificationWorkflow(e.target.value.id) + toast("Updated notification workflow. Don't forget to save!") + } + + const orgSaveButton = ( + +
+ +
+
+ ); + + const toggleBetweenRequiredOrOptional = (event) => { + setSSORequired(event.target.checked); + }; + + return ( +
+ + + + Notification Workflow + + {/* + + */} + + +
+ {workflows !== undefined && workflows !== null && workflows.length > 0 ? + { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { + setNotificationWorkflow(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + } +
+ {orgSaveButton} +
+
+
+
+ + + Org Documentation reference + { + setDocumentationReference(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + -import { - ExpandLess as ExpandLessIcon, - ExpandMore as ExpandMoreIcon, - Save as SaveIcon, -} from "@mui/icons-material"; - -const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important", - }, -}) - -const OrgHeaderexpanded = (props) => { - const { - userdata, - selectedOrganization, - setSelectedOrganization, - globalUrl, - isCloud, - adminTab, - } = props; - - const classes = useStyles(); - const defaultBranch = "master"; - - const [orgName, setOrgName] = React.useState(selectedOrganization.name); - const [orgDescription, setOrgDescription] = React.useState( - selectedOrganization.description - ); - - const [appDownloadUrl, setAppDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo === undefined || - selectedOrganization.defaults.app_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo - ); - const [appDownloadBranch, setAppDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.app_download_branch === undefined || - selectedOrganization.defaults.app_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.app_download_branch - ); - const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.workflow_download_repo === undefined || - selectedOrganization.defaults.workflow_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-workflows" - : selectedOrganization.defaults.workflow_download_repo - ); - const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch === undefined || - selectedOrganization.defaults.workflow_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch - ); - const [ssoEntrypoint, setSsoEntrypoint] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_entrypoint === undefined || - selectedOrganization.sso_config.sso_entrypoint.length === 0 - ? "" - : selectedOrganization.sso_config.sso_entrypoint - ); - const [ssoCertificate, setSsoCertificate] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_certificate === undefined || - selectedOrganization.sso_config.sso_certificate.length === 0 - ? "" - : selectedOrganization.sso_config.sso_certificate - ); - const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined - ? false - : selectedOrganization.sso_config.SSORequired === undefined - ? false - : selectedOrganization.sso_config.SSORequired); - - const [notificationWorkflow, setNotificationWorkflow] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.notification_workflow === undefined || - selectedOrganization.defaults.notification_workflow.length === 0 - ? "" - : selectedOrganization.defaults.notification_workflow - ); - - const [documentationReference, setDocumentationReference] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.documentation_reference === undefined || - selectedOrganization.defaults.documentation_reference.length === 0 - ? "" - : selectedOrganization.defaults.documentation_reference - ); - const [openidClientId, setOpenidClientId] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_id === undefined || - selectedOrganization.sso_config.client_id.length === 0 - ? "" - : selectedOrganization.sso_config.client_id - ); - const [openidClientSecret, setOpenidClientSecret] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_secret === undefined || - selectedOrganization.sso_config.client_secret.length === 0 - ? "" - : selectedOrganization.sso_config.client_secret - ); - const [openidAuthorization, setOpenidAuthorization] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_authorization === undefined || - selectedOrganization.sso_config.openid_authorization.length === 0 - ? "" - : selectedOrganization.sso_config.openid_authorization - ); - const [openidToken, setOpenidToken] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_token === undefined || - selectedOrganization.sso_config.openid_token.length === 0 - ? "" - : selectedOrganization.sso_config.openid_token - ) - - const [workflows, setWorkflows] = React.useState([]) - const [workflow, setWorkflow] = React.useState({}) - - const getAvailableWorkflows = (trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { - 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) { - setWorkflows(responseJson) - - if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { - - const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) - if (workflow !== undefined && workflow !== null) { - setWorkflow(workflow) - } - } - } - }) - .catch((error) => { - console.log("Error getting workflows: " + error); - }) - } - - useEffect(() => { - getAvailableWorkflows() - }, []) - - const handleEditOrg = ( - name, - description, - orgId, - image, - defaults, - sso_config - ) => { - - const data = { - name: name, - description: description, - org_id: orgId, - image: image, - defaults: defaults, - sso_config: sso_config, - }; - - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - toast("Failed updating org: ", responseJson.reason); - } else { - toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - toast("Err: " + error.toString()); - }); - }; - - - const handleWorkflowSelectionUpdate = (e, isUserinput) => { - if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { - console.log("Returning as there's no id") - return null - } - - setWorkflow(e.target.value) - setNotificationWorkflow(e.target.value.id) - toast("Updated notification workflow. Don't forget to save!") - } - - const orgSaveButton = ( - -
- -
-
- ); - - const toggleBetweenRequiredOrOptional = (event) => { - if (ssoEntrypoint === "" && openidAuthorization === "" && openidToken === "") { - if (!SSORequired) { - toast.error("Please fill in fields for either OpenID connect or SSO before continuing. ") - return - } - } else { - toast.info("Toggled SSO. Remember to save.") - } - - setSSORequired(event.target.checked) - }; - - return ( -
- - - - Notification Workflow - - {/* - - */} - - -
- {workflows !== undefined && workflows !== null && workflows.length > 0 ? - { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return "No Workflow Selected"; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={workflows} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette.borderRadius, - }} - onChange={(event, newValue) => { - console.log("Found value: ", newValue) - - var parsedinput = { target: { value: newValue } } - - // For variables - if (typeof newValue === 'string' && newValue.startsWith("$")) { - parsedinput = { - target: { - value: { - "name": newValue, - "id": newValue, - "actions": [], - "triggers": [], - } - } - } - } - - handleWorkflowSelectionUpdate(parsedinput) - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } - - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose {data.name} - - - } placement="bottom"> - { - var parsedinput = { target: { value: data } } - handleWorkflowSelectionUpdate(parsedinput) - }} - > - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - : - { - setNotificationWorkflow(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - } -
- {orgSaveButton} -
-
-
-
- - - Org Documentation reference - { - setDocumentationReference(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - {/* {isCloud ? null : */} - Single Signon - SAML/OpenID -
- Make SAML SSO or OpenID Authentication required/optional for your organization. Tip: Do not make it required until you have tested the URL directly. -
- { - toggleBetweenRequiredOrOptional(e) - }} - name="onOffSwitch" - color="primary" - title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization" - /> - {SSORequired ? 'Required' : 'Optional'} -
-
-
-
- - OpenID connect - - - - Client ID - { - setOpenidClientId(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - Client Secret (optional) - { - setOpenidClientSecret(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - - - Authorization URL - { - setOpenidAuthorization(e.target.value) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - Token URL - { - setOpenidToken(e.target.value) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - {/* } */} - {/*isCloud ? null : */} - SAML SSO (v1.1) - + Workflow Backup Repository + + Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'status_workflowid.json' + + - SSO Entrypoint (IdP) + Repository for workflow backup { - setSsoEntrypoint(e.target.value); + setUploadRepo(e.target.value); }} InputProps={{ classes: { - notchedOutline: classes.notchedOutline, + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { color: "white", @@ -715,14 +554,14 @@ const OrgHeaderexpanded = (props) => { - SSO Certificate (X509) + Branch { margin="normal" variant="outlined" multiline={true} - rows={2} - placeholder="The X509 certificate to use" - value={ssoCertificate} + rows={1} + placeholder="The branch to use for backup of workflows" + value={uploadBranch} onChange={(e) => { - setSsoCertificate(e.target.value); + setUploadBranch(e.target.value); }} InputProps={{ classes: { - notchedOutline: classes.notchedOutline, + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { color: "white", @@ -748,164 +587,485 @@ const OrgHeaderexpanded = (props) => { - {isCloud ? - - IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso - - : null} + + + + Username + { + setUploadUsername(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Git token/password + { + setUploadToken(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + type="password" + /> + + + - {isCloud ? null : ( - - - App Download URL - { - setAppDownloadUrl(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - App Download Branch - { - setAppDownloadBranch(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - Workflow Download URL - { - setWorkflowDownloadUrl(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - Workflow Download Branch - { - setWorkflowDownloadBranch(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} -
- {orgSaveButton} -
- {/* - - {expanded ? - - : - - } - - */} -
-
- ) -} + + SSO Configuration + +
+ Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. +
+ + {SSORequired ? 'Required' : 'Optional'} +
+
+ + OpenID connect + + + + Client ID + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The OpenID client ID from the identity provider" + value={openidClientId} + onChange={(e) => { + setOpenidClientId(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Client Secret (optional) + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE" + value={openidClientSecret} + onChange={(e) => { + setOpenidClientSecret(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + + + Authorization URL + { + setOpenidAuthorization(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Token URL + { + setOpenidToken(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + {/* } */} + {/*isCloud ? null : */} + + SAML SSO (v1.1) + + + + SSO Entrypoint (IdP) + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The entrypoint URL from your provider" + value={ssoEntrypoint} + onChange={(e) => { + setSsoEntrypoint(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + SSO Certificate (X509) + { + setSsoCertificate(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + {isCloud ? + + IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso + + : null} + + {isCloud ? null : ( + + + App Download URL + { + setAppDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + App Download Branch + { + setAppDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download URL + { + setWorkflowDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download Branch + { + setWorkflowDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + +
+ {orgSaveButton} +
+ {/* + + {expanded ? + + : + + } + + */} +
+
+ ) +} + export default OrgHeaderexpanded; diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 29297082..4d73ebdd 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -167,6 +167,7 @@ const ParsedAction = (props) => { listCache, + authGroups, apps, setEditorData, setcodedata, @@ -174,14 +175,30 @@ const ParsedAction = (props) => { } = props; const classes = useStyles(); - const [hideBody, setHideBody] = React.useState(true); - const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false); - + const [hideBody, setHideBody] = React.useState(true) + const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false) + const [appActionName, setAppActionName] = React.useState(selectedAction.label); + const [delay, setDelay] = React.useState(selectedAction?.execution_delay || 0); + const [fieldCount, setFieldCount] = React.useState(0); const [hiddenDescription, setHiddenDescription] = React.useState(true); - const [autoCompleting, setAutocompleting] = React.useState(false); - + const [selectedActionParameters, setSelectedActionParameters] = React.useState(selectedAction?.parameters || []); + const [selectedVariableParameter, setSelectedVariableParameter] = React.useState(""); + const [paramValues, setParamValues] = React.useState( + selectedAction?.parameters.map((param) => { + return { + name: param.name, + value: param.value, + } + }) + ); + const [actionlist, setActionlist] = React.useState([]); + const [jsonList, setJsonList] = React.useState([]); + const [showDropdown, setShowDropdown] = React.useState(false); + const [showDropdownNumber, setShowDropdownNumber] = React.useState(0); + const [showAutocomplete, setShowAutocomplete] = React.useState(false); + const [menuPosition, setMenuPosition] = useState(null); const isIntegration = selectedAction.app_id === "integration" useEffect(() => { @@ -189,12 +206,33 @@ const ParsedAction = (props) => { setLastSaved(false) } }, [expansionModalOpen]) + useEffect(() => { + setParamValues(selectedAction.parameters.map((param) => { + return { + name: param.name, + value: param.value, + } + })) + },[ + selectedAction, selectedApp,setNewSelectedAction, workflow, + ]) + + useEffect(() => { + setParamValues(selectedAction.parameters.map((param) => { + return { + name: param.name, + value: param.value, + } + })) + },[ + selectedAction, selectedApp,setNewSelectedAction, workflow, + ]) useEffect(() => { if (selectedAction.parameters === null || selectedAction.parameters === undefined) { return } - + const paramcheck = selectedAction.parameters.find(param => param.name === "body") if (paramcheck === undefined || paramcheck === null) { return @@ -205,12 +243,11 @@ const ParsedAction = (props) => { setHideBody(true) } else { setHideBody(false) - + if (paramcheck.id === "UNTOGGLED") { setActivateHidingBodyButton(false) } } - }, []) const keywords = [ @@ -371,252 +408,187 @@ const ParsedAction = (props) => { //setStartNode(selectedAction.id) }; - const AppActionArguments = (props) => { - const [selectedActionParameters, setSelectedActionParameters] = React.useState([]); - const [selectedVariableParameter, setSelectedVariableParameter] = React.useState(""); - const [actionlist, setActionlist] = React.useState([]); - const [jsonList, setJsonList] = React.useState([]); - const [showDropdown, setShowDropdown] = React.useState(false); - const [showDropdownNumber, setShowDropdownNumber] = React.useState(0); - const [showAutocomplete, setShowAutocomplete] = React.useState(false); - const [menuPosition, setMenuPosition] = useState(null); - - useEffect(() => { - if (selectedActionParameters !== undefined && selectedActionParameters !== null && selectedActionParameters.length === 0 - ) { - if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > 0) { - setSelectedActionParameters(selectedAction.parameters); - } - } - - if ((selectedVariableParameter === null || selectedVariableParameter === undefined) && workflow.workflow_variables !== null && workflow.workflow_variables.length > 0) { - - // FIXME - this is the bad thing - setSelectedVariableParameter(workflow.workflow_variables[0].name); - } - - if (actionlist.length === 0) { - // FIXME: Have previous execution values in here - if (workflowExecutions.length > 0) { - for (let [key,keyval] in Object.entries(workflowExecutions)) { - if ( - workflowExecutions[key].execution_argument === undefined || - workflowExecutions[key].execution_argument === null || - workflowExecutions[key].execution_argument.length === 0 - ) { - continue; - } - - const valid = validateJson(workflowExecutions[key].execution_argument) - if (valid.valid) { - actionlist.push({ - type: "Execution Argument", - name: "Execution Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: valid.result, - }) - break - } - } + useEffect( + () => { + + // Only set app action name if it has changed + if (selectedAction.label !== appActionName) { + setAppActionName(selectedAction.label); } - - if (actionlist.length === 0) { - actionlist.push({ - type: "Execution Argument", - name: "Execution Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: "", - }) + + // Only set delay if it has changed + const newDelay = selectedAction?.execution_delay || 0; + if (newDelay !== delay) { + setDelay(newDelay); } + + // Only set selected action parameters if they have changed + if (selectedAction.parameters && selectedAction.parameters.length > 0) { + setSelectedActionParameters(selectedAction.parameters); + } + + // Only set selected variable parameter if it is null or undefined + if (!selectedVariableParameter && workflow.workflow_variables?.length > 0) { + setSelectedVariableParameter(workflow.workflow_variables[0].name); + } + + + }, + [selectedAction,selectedApp,setNewSelectedAction,workflow, workflowExecutions, getParents] + ); - /* - actionlist.push({ - type: "Shuffle DB", - name: "Shuffle DB", - value: "$shuffle_cache", - highlight: "shuffle_cache", - autocomplete: "shuffle_cache", - example: { - "what": "", - "unique gmail ids new": "", - }, - }) - */ + useEffect(() => { + const newActionList = []; - var cachekey = { - type: "Shuffle DB", - name: "Shuffle DB", - value: "$shuffle_cache", - highlight: "shuffle_cache", - autocomplete: "shuffle_cache", - example: "", + // Process workflowExecutions + if (workflowExecutions.length > 0) { + for (let execution of workflowExecutions) { + const execArg = execution.execution_argument; + if (execArg && execArg.length > 0) { + const valid = validateJson(execArg); + if (valid.valid) { + newActionList.push({ + type: "Execution Argument", + name: "Execution Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: valid.result, + }); + break; + } + } + } } - if (listCache !== undefined && listCache !== null && listCache.keys !== undefined && listCache.keys !== null && listCache.keys.length > 0) { - cachekey.example = {} - - for (var i in listCache.keys) { - const item = listCache.keys[i] - if (item.key === undefined || item.key === null || item.key.length === 0) { - continue - } - - var itemvalue = item.value === undefined || item.value === null ? "" : item.value - try{ - if (itemvalue.length > 10000) { - itemvalue = "" - } - - } catch (e) { - itemvalue = "" - } - - var itemkey = item.key.split(" ").join("_") - cachekey.example[itemkey] = { - "value": itemvalue, - } - } - } else { - } - - actionlist.push(cachekey) - - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - for (let [key,keyval] in Object.entries(workflow.workflow_variables)) { - const item = workflow.workflow_variables[key]; - actionlist.push({ - type: "workflow_variable", - name: item.name, - value: item.value, - id: item.id, - autocomplete: `${item.name.split(" ").join("_")}`, - example: item.value, + // Add default Execution Argument if none were added + if (newActionList.length === 0) { + newActionList.push({ + type: "Execution Argument", + name: "Execution Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "", }); - } + + let cacheKey = { + type: "Shuffle DB", + name: "Shuffle DB", + value: "$shuffle_cache", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", + example: "", + }; + + if (listCache?.keys?.length > 0) { + cacheKey.example = {}; + for (let item of listCache.keys) { + if (item.key) { + let itemValue = item.value ?? ""; + if (itemValue.length > 10000) { + itemValue = ""; + } + cacheKey.example[item.key.split(" ").join("_")] = { value: itemValue }; + } + } + } + + newActionList.push(cacheKey); } - if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) { - for (let [key,keyval] in Object.entries(workflow.execution_variables)) { - const item = workflow.execution_variables[key] - - var exampleoutput = "" - for (let execkey in workflowExecutions) { - const exec = workflowExecutions[execkey] - if (exec["execution_variables"] === undefined || exec["execution_variables"] === null) { - continue - } - - const foundExec = exec.execution_variables.find((exvar) => exvar.name === item.name) - if (!foundExec) { - continue - } - - if (foundExec.value !== undefined && foundExec.value !== null && foundExec.value.length > 0) { - exampleoutput = foundExec.value - break - } - } - - actionlist.push({ - type: "execution_variable", - name: item.name, - value: item.value, - id: item.id, - autocomplete: `${item.name.split(" ").join("_")}`, - example: exampleoutput, - }); - } + // Process workflow variables + if (workflow.workflow_variables?.length > 0) { + for (let variable of workflow.workflow_variables) { + newActionList.push({ + type: "workflow_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: variable.value, + }); + } } - // Loops parent nodes' old results to fix autocomplete - if (getParents !== undefined) { - var parents = getParents(selectedAction) + // Process execution variables + if (workflow.execution_variables?.length > 0) { + for (let variable of workflow.execution_variables) { + let exampleOutput = ""; + for (let exec of workflowExecutions) { + const foundExec = exec.execution_variables?.find(exvar => exvar.name === variable.name); + if (foundExec?.value) { + exampleOutput = foundExec.value; + break; + } + } + newActionList.push({ + type: "execution_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: exampleOutput, + }); + } + } - if (parents.length > 1) { - var labels = [] - //for (let [parentkey, parentkeyval] in Object.entries(parents)) { - for (let parentkey in parents) { - const parentNode = parents[parentkey] - if (parentNode.label === "Execution Argument") { - continue - } + // Process parent actions if getParents is provided + if (getParents) { + const parents = getParents(selectedAction); + if (parents.length > 1) { + const labels = []; + for (let parentNode of parents) { + if (parentNode.label !== "Execution Argument" && !labels.includes(parentNode.label)) { + labels.push(parentNode.label); + let exampleData = parentNode.example ?? ""; + if (!exampleData && workflowExecutions.length > 0) { + for (let exec of workflowExecutions) { + const foundResult = exec.results?.find(result => result.action.id === parentNode.id); + if (foundResult) { + const valid = validateJson(foundResult.result); + if (valid.valid && valid.result.success !== false) { + exampleData = valid.result; + break; + } + } + } + } + newActionList.push({ + type: "action", + id: parentNode.id, + name: parentNode.label, + autocomplete: parentNode.label.split(" ").join("_"), + example: exampleData, + }); + } + } + } + } - //if (labels.includes(item.label)) { - // continue - //} + // Update the actionlist state + setActionlist(newActionList); + }, [workflow.execution_variables, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents]); + + useEffect(() => { + selectedNameChange(appActionName) - labels.push(parentNode.label) - - var exampledata = parentNode.example === undefined || parentNode.example === null ? "" : parentNode.example - // Find previous execution and their variables - //exampledata === "" && - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false; - for (let wfkey in workflowExecutions) { - if (workflowExecutions[wfkey].results === undefined || workflowExecutions[wfkey].results === null) { - - continue; - } - - var foundResult = workflowExecutions[wfkey].results.find((result) => result.action.id === parentNode.id) - - if (foundResult === undefined || foundResult === null) { - continue - } - - if (foundResult.result !== undefined && foundResult.result !== null) { - foundResult = foundResult.result - } - - const valid = validateJson(foundResult) - if (valid.valid) { - if (valid.result.success === false) { - //console.log("Skipping success false autocomplete") - } else { - - // FIXME: Have a merge system to allow to use kind of any key from that node in the last 10-20 execs - //if (exampledata.length > 0) { - // exampledata = valid.result - //} else { - // exampledata = valid.result - //} - - exampledata = valid.result - break - } - } else { - exampledata = foundResult - } - } - } - - // 1. Take - const itemlabelComplete = parentNode.label === null || parentNode.label === undefined ? "" : parentNode.label.split(" ").join("_"); - - const actionvalue = { - type: "action", - id: parentNode.id, - name: parentNode.label, - autocomplete: itemlabelComplete, - example: exampledata, - } - - actionlist.push(actionvalue) - } - } - - setActionlist(actionlist); + if (actionDelayChange !== undefined) { + actionDelayChange(delay) + } + },[appActionName,delay]) + + const handleParamChange = (event, count,data) => { + const newParams = [...paramValues]; + newParams.map((param) => { + if (param.name === data.name) { + param.value = event.target.value; + } + }) + setParamValues(newParams); + changeActionParameter(event, count, data) } - } - }); - - const calculateHelpertext = (input_data) => { var helperText = "" var looperText = "" @@ -842,9 +814,9 @@ const ParsedAction = (props) => { //console.log("CHANGING ACTION COUNT !") selectedActionParameters[count].autocompleted = false - selectedAction.parameters[count].autocompleted = false - selectedActionParameters[count].value = event.target.value; - selectedAction.parameters[count].value = event.target.value; + selectedAction.parameters[count].autocompleted = false + selectedActionParameters[count].value = event.target.value; + selectedAction.parameters[count].value = event.target.value; var forceUpdate = false if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { @@ -854,7 +826,7 @@ const ParsedAction = (props) => { if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && data.name === "input_list") { //console.log("FILTER LIST!: ", event, count, data) const parsedvalue = event.target.value - if (parsedvalue.includes("#")) { + if (parsedvalue.includes(".#")) { const splitparsed = parsedvalue.split(".#.") //console.log("Cant contain #: ", splitparsed) if (splitparsed.length > 1) { @@ -863,14 +835,30 @@ const ParsedAction = (props) => { selectedActionParameters[count].value = splitparsed[0] selectedAction.parameters[count].value = splitparsed[0] - selectedActionParameters[1].value = splitparsed[1] - selectedAction.parameters[1].value = splitparsed[1] - forceUpdate = true + selectedActionParameters[1].value = splitparsed[1] + selectedAction.parameters[1].value = splitparsed[1] + } else { + // Remove .# and after + const splitparsed = parsedvalue.split(".#") + data.value = splitparsed[0] + selectedActionParameters[0].value = splitparsed[0] + selectedAction.parameters[0].value = splitparsed[0] + + selectedActionParameters[1].value = "" + selectedAction.parameters[1].value = "" + + toast.warn("No value found in the list. Please select an item in the list to filter based on.") } + + forceUpdate = true + selectedActionParameters[0].autocompleted = true + selectedAction.parameters[0].autocompleted = true + selectedActionParameters[1].autocompleted = true + selectedAction.parameters[1].autocompleted = true } } - setSelectedAction(selectedAction); + setSelectedAction(selectedAction); if (forceUpdate || viewForceUpdate === true) { setUpdate(Math.random()) } @@ -941,6 +929,7 @@ const ParsedAction = (props) => { } } + // bad detection mechanism probably if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) { console.log("GET THE LAST ARGUMENT FOR NODE!") @@ -1014,10 +1003,12 @@ const ParsedAction = (props) => { } } + setTimeout(() => { selectedActionParameters[count].autocompleted = false selectedAction.parameters[count].autocompleted = false selectedActionParameters[count].value = data selectedAction.parameters[count].value = data + }, 100); setSelectedAction(selectedAction) //setUpdate(Math.random()) //setUpdate(event.target.value) @@ -1123,10 +1114,68 @@ const ParsedAction = (props) => { return helperText } + const analyzeFields = () => { + + if (selectedAction === undefined || selectedAction === null) { + return null + } + + if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) { + return null + } + + // Only shuffle tools for now + if (selectedAction.app_name !== "Shuffle Tools") { + return null + } + + // Custom rules + if (selectedAction.name === "set_cache_value") { + var actionKey = "" + var actionValue = "" + for (let [key,keyval] in Object.entries(selectedActionParameters)) { + const param = selectedActionParameters[key] + if (param.name === "key") { + actionKey = param.value + } + + if (param.name === "value") { + actionValue = param.value + } + } + + if (actionKey === "" || actionValue === "") { + return null + } + + if (!actionKey.includes(".#") && actionValue.includes(".#")) { + return When the key ({actionKey}) is static, but the value is a list ({actionValue}), it will overwrite the list. You may be looking for the {}} style={{cursor: "pointer", color: "#f85a3e", }}>Check Cache Contains action instead. + } + } + + return null + + } + + const suggestionInfo = () => { + const suggestionText = analyzeFields() + if (suggestionText === undefined || suggestionText === null) { + return null + } + + if (selectedAction.errors === undefined || selectedAction.errors === null || selectedAction.errors.length === 0) { + selectedAction.errors = ["Suggestion: " + suggestionText] + } + + return + + Tip: {suggestionText} + + + } // FIXME: Issue #40 - selectedActionParameters not reset - if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { - + if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { var wrapperapp = { "id": "", "name": "noapp", @@ -1152,13 +1201,1209 @@ const ParsedAction = (props) => { var authWritten = false; var noAppSelected = false - const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + var paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") if (paramIndex === -1 || selectedAction.parameters[paramIndex].value === "" || selectedAction.parameters[paramIndex].value === "noapp") { // Check the actual value and if it's the same noAppSelected = true } - return ( -
+ } + + + const ActionSelectOption = (actionprops) => { + const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops; + const [hover, setHover] = React.useState(false); + + return ( + +
setHover(true)} onMouseLeave={() => setHover(false)} + onClick={(event) => { + // event.preventDefault() + //setSelectedAction(actionprops) + //setShowActionList(false) + //setUpdate(Math.random()) + // + if (option !== undefined && option !== null) { + setNewSelectedAction({ + target: { + value: option.name + } + }); + } + document.activeElement.blur(); + }} + > +
+ + {useIcon} + + {newActionname} +
+ {extraDescription.length > 0 ? + + {extraDescription} + + : null} +
+
+ ) + } + + const sortByCategoryLabel = (a, b) => { + const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 + const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 + + // Sort by existence and length of "category_label" + if (aHasCategoryLabel && !bHasCategoryLabel) { + return -1 + } else if (!aHasCategoryLabel && bHasCategoryLabel) { + return 1 + } else { + return 0 + } + } + + // Function to deduplicate based on the "name" field + const deduplicateByName = (array) => { + const uniqueNames = {}; + return array.filter(item => { + if (!item.hasOwnProperty('name') || !item.name.length) { + return true + } + if (!uniqueNames[item.name]) { + uniqueNames[item.name] = true + return true + } + return false + }) + } + + // Gets the most important actions first + const renderedActionOptions = deduplicateByName(( + selectedApp.actions === undefined || selectedApp.actions === null ? [] : + selectedApp.actions.filter((a) => + a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label")) + ).sort(sortByCategoryLabel)) + + + const selectedAppIcon = selectedAction.large_image + var baselabel = selectedAction.label + return ( +
+ + {hideExtraTypes === true ? null : ( + +
+
+
{ + //window.open("/apps/${selectedAction.app_id}", "_blank") + }} + > + + + +

+ {( + selectedAction.app_name.charAt(0).toUpperCase() + + selectedAction.app_name.substring(1) + ).replaceAll("_", " ")} +

+
+
+ { + if (workflowExecutions.length > 0) { + // Look for the ID + var 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 === selectedAction.id + ) + + if (foundResult === undefined || foundResult === null) { + continue; + } + + const oldstartnode = cy.getElementById(selectedAction.id); + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + foundResult.action.label = foundname + } + } + + setSelectedResult(foundResult); + if (setCodeModalOpen !== undefined) { + setCodeModalOpen(true); + + found = true + } + + break; + } + + if (!found) { + toast("No result for this action yet. Please run the workflow first.") + } + } + }} + > + + + + + { + setAuthenticationModalOpen(true) + }} + > + + + + + {/* + {}} + > + + + + + + + */} + {/* + { + //setAuthenticationModalOpen(true); + console.log("Should enable/disable magic!") + console.log("Action: ", selectedAction) + if (selectedAction.run_magic_output === undefined) { + selectedAction.run_magic_output = true + } else { + if (selectedAction.run_magic_output === true) { + selectedAction.run_magic_output = false + } else { + selectedAction.run_magic_output = true + } + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()); + }} + > + + + + + */} + {/* + { + }} + > + + + + + + + */} + { + //if (setAiQueryModalOpen !== undefined) { + // setAiQueryModalOpen(true) + //} else { + aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) + //} + setAutocompleting(true) + }} + > + + {autoCompleting ? + + : + + } + + +
+
+
+ {/*selectedAction.id === workflow.start ? null : + + + + + */} + {selectedApp.versions !== null && + selectedApp.versions !== undefined && + selectedApp.versions.length > 1 ? ( + + ) : null} +
+
+
+
+ Name + { + let newValue = event.target.value + newValue = newValue.replaceAll(" ", "_") + setAppActionName(newValue) + } + } + onBlur={(e) => { + // Copy the name value + const name = appActionName + const parsedBaseLabel = "$"+baselabel.toLowerCase().replaceAll(" ", "_") + const newname = "$"+name.toLowerCase().replaceAll(" ", "_") + + // Check if it's the same as the current name in use + //if (name === selectedAction.label) { + // console.log("Returning from name thing") + // return + //} + + // Change in actions, triggers & conditions + // Highlight the changes somehow with a glow? + if (workflow.branches !== undefined && workflow.branches !== null) { + for (let [key,keyval] in Object.entries(workflow.branches)) { + if (workflow.branches[key].conditions !== undefined && workflow.branches[key].conditions !== null) { + for (let [subkey,subkeyval] in Object.entries(workflow.branches[key].conditions)) { + const condition = workflow.branches[key].conditions[subkey] + const sourceparam = condition.source + const destinationparam = condition.destination + + // Should have a smarter way of discovering node names + // Finding index(es) and replacing at the location + if (sourceparam.value.includes("$")) { + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + + const foundindex = sourceparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex+newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (sourceparam.value.length > foundindex+parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = sourceparam.value[foundindex+parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + console.log("Old found: ", workflow.branches[key].conditions[subkey].source.value) + const extralength = newname.length-parsedBaseLabel.length + sourceparam.value = sourceparam.value.substring(0, foundindex) + newname + sourceparam.value.substring(foundindex-extralength+newname.length, sourceparam.value.length) + + console.log("New: ", workflow.branches[key].conditions[subkey].source.value) + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + + if (destinationparam.value.includes("$")) { + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + + const foundindex = destinationparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex+newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (destinationparam.value.length > foundindex+parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = destinationparam.value[foundindex+parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + console.log("Old found: ", workflow.branches[key].conditions[subkey].destination.value) + const extralength = newname.length-parsedBaseLabel.length + destinationparam.value = destinationparam.value.substring(0, foundindex) + newname + destinationparam.value.substring(foundindex-extralength+newname.length, destinationparam.value.length) + + console.log("New: ", workflow.branches[key].conditions[subkey].destination.value) + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + } + } + } + } + + for (let [key,keyval] in Object.entries(workflow.actions)) { + if (workflow.actions[key].id === selectedAction.id) { + continue + } + + const params = workflow.actions[key].parameters + console.log(params) + if (params === null || params === undefined) { + continue + } + + for (let [subkey, subkeyval] in Object.entries(params)) { + const param = workflow.actions[key].parameters[subkey]; + if (!param.value.includes("$")) { + continue + } + + // Should have a smarter way of discovering node names + // Do regex? + // Finding index(es) and replacing at the location + // + + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + + const foundindex = param.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex+newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (param.value.length > foundindex+parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = param.value[foundindex+parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + console.log("Old found: ", workflow.actions[key].parameters[subkey].value) + const extralength = newname.length-parsedBaseLabel.length + param.value = param.value.substring(0, foundindex) + newname + param.value.substring(foundindex-extralength+newname.length, param.value.length) + + console.log("New: ", workflow.actions[key].parameters[subkey].value) + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + } + + setWorkflow(workflow); + setUpdate(Math.random()); + baselabel = name + }} + /> +
+ {/*!isCloud ? null :*/} +
+ + + Delay + { + setDelay(event.target.value) + }} + /> + + +
+ {/**/} +
+
+ )} + {selectedApp.name !== undefined && + selectedAction.authentication !== null && + selectedAction.authentication !== undefined && + selectedAction.authentication.length === 0 && + requiresAuthentication ? ( +
+ + + + + +
+ ) : null} + + {selectedAction.authentication !== undefined && + selectedAction.authentication !== null && + selectedAction.authentication.length > 0 ? ( +
+ Authentication +
+ + + {/* + + + curaction.authentication = authenticationOptions + if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") + */} + + { + setAuthenticationModalOpen(true); + }} + > + + + +
+
+ ) : null} + + {showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? ( +
+ Environment + +
+ ) : null} + {workflow.execution_variables !== undefined && + workflow.execution_variables !== null && + workflow.execution_variables.length > 0 ? ( +
+ Execution variable (optional) + +
+ ) : null} + + +
+ {/*hideExtraTypes ? null : +
+ Actions +
+ */} + + {setNewSelectedAction !== undefined ? ( + { + // Most popular + // Is categorized + // Uncategorized + return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; + }} + renderGroup={(params) => { + + return ( +
  • + {params.group} + {params.children} +
  • + ) + }} + options={renderedActionOptions} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + }, + }} + filterOptions={(options, { inputValue }) => { + //console.log("Option contains?: ", inputValue, options) + const lowercaseValue = inputValue.toLowerCase() + options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) + + return options + }} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.name === undefined || option.name === null ) { + return null; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + + return newname; + }} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + // Workaround with event lol + if (newValue !== undefined && newValue !== null) { + setNewSelectedAction({ + target: { + value: newValue.name + } + }); + } + }} + renderOption={(props, option, state) => { + var newActionname = option.name; + if (option.label !== undefined && option.label !== null && option.label.length > 0) { + newActionname = option.label; + } + + var newActiondescription = option.description; + //console.log("DESC: ", newActiondescription) + if (option.description === undefined || option.description === null) { + newActiondescription = "Description: No description defined for this action" + } else { + newActiondescription = "Description: "+newActiondescription + } + + const iconInfo = GetIconInfo({ name: option.name }); + const useIcon = iconInfo.originalIcon; + + if (newActionname === undefined || newActionname === null) { + newActionname = "No name" + option.name = "No name" + option.label = "No name" + } + + newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); + + var method = "" + var extraDescription = "" + if (option.name.includes("get_")) { + method = "GET" + } else if (option.name.includes("post_")) { + method = "POST" + } else if (option.name.includes("put_")) { + method = "PUT" + } else if (option.name.includes("patch_")) { + method = "PATCH" + } else if (option.name.includes("delete_")) { + method = "DELETE" + } else if (option.name.includes("options_")) { + method = "OPTIONS" + } else if (option.name.includes("connect_")) { + method = "CONNECT" + } + + // FIXME: Should it require a base URL? + if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) { + var extraUrl = "" + const descSplit = option.description.split("\n") + // Last line of descSplit + if (descSplit.length > 0) { + extraUrl = descSplit[descSplit.length-1] + } + + //for (let [line,lineval] in Object.entries(descSplit)) { + // if (descSplit[line].includes("http") && descSplit[line].includes("://")) { + // const urlsplit = descSplit[line].split("/") + // try { + // extraUrl = "/"+urlsplit.slice(3, urlsplit.length).join("/") + // } catch (e) { + // //console.log("Failed - running with -1") + // extraUrl = "/"+urlsplit.slice(3, urlsplit.length-1).join("/") + // } + + + // //console.log("NO BASEURL TOO!! Why missing last one in certain scenarios (sevco)?", extraUrl, urlsplit, descSplit[line]) + // //break + // } + //} + + if (extraUrl.length > 0) { + if (extraUrl.includes(" ")) { + extraUrl = extraUrl.split(" ")[0] + } + + if (extraUrl.includes("#")) { + extraUrl = extraUrl.split("#")[0] + } + + extraDescription = `${method} ${extraUrl}` + } else { + //console.log("No url found. Check again :)") + } + } + + return ( + + ); + }} + renderInput={(params) => { + if (params.inputProps?.value) { + const prefixes = ["Post", "Put", "Patch"]; + for (let prefix of prefixes) { + if (params.inputProps.value.startsWith(prefix)) { + let newValue = params.inputProps.value.replace(prefix + " ", ""); + if (newValue.length > 1) { + newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1); + } + // Set the new value without mutating inputProps + params = { ...params, inputProps: { ...params.inputProps, value: newValue } }; + break; + } + } + // Check if it starts with "Get List" and method is "Get" + if (params.inputProps.value.startsWith("Get List")) { + console.log("Get List"); + } + } + + return ( + + ); + }} + /> + ) : null} + + {/*setNewSelectedAction !== undefined ? + + : null*/} +
    { + Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ? +
    {isIntegration ? apps !== undefined && apps !== null && apps.length > 0 ?
    @@ -1310,6 +2555,7 @@ const ParsedAction = (props) => { } + {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ?
    @@ -1441,6 +2687,8 @@ const ParsedAction = (props) => {
    ) : null} + {suggestionInfo()} + {selectedActionParameters.map((data, count) => { if (data.variant === "") { data.variant = "STATIC_VALUE"; @@ -1526,9 +2774,9 @@ const ParsedAction = (props) => { placeholder = data.example; - if (data.name === "url" && data.value !== undefined && data.value !== null && data.value.length === 0) { - data.value = data.example; - } + // if (data.name === "url") { + // data.value = data.example; + // } // In case of data.example if (data.value === undefined || data.value === null) { data.value = "" @@ -1608,186 +2856,181 @@ const ParsedAction = (props) => { //setSelectedActionParameters(selectedActionParameters) } - var hideBodyButton = ""; - const hideBodyButtonValue = ( -
    - - { - var tag = "TOGGLED" - if (hideBody) { - tag = "UNTOGGLED" - } + var hideBodyButton = ""; + const hideBodyButtonValue = ( +
    + + { + const newHideBody = !hideBody; + setHideBody(newHideBody) + + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { ...param, id: newHideBody ? "UNTOGGLED" : "TOGGLED" }; + } + if (param.description === openApiFieldDesc) { + return { ...param, field_active: newHideBody }; + } + return param; + }); + + setSelectedActionParameters(updatedParameters); + + /* + setTimeout(() => { + const element = document.getElementById("hide_body_button"); + if (element) { + element.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + }, 100); + */ + }} + name="requires_unique" + /> + } + label={hideBody ? "Show Body" : "Hide Body"} + /> + +
    + ); - setHideBody(!hideBody) - for (let paramkey in Object.entries(selectedActionParameters)) { - var currentItem = selectedActionParameters[paramkey]; - if (currentItem.name === "ssl_verify") { + if (selectedApp.generated && data.name === "body") { + const regex = /\${(\w+)}/g; + const found = placeholder.match(regex); - } + hideBodyButton = hideBodyButtonValue; + if (found === null || !hideBody) { + if (found === null) { - if (currentItem.name === "body") { - currentItem.id = tag - } + if (activateHidingBodyButton !== true) { + setActivateHidingBodyButton(true) + } - if (currentItem.description === openApiFieldDesc) { - currentItem.field_active = !hideBody - } - } - - - // Scroll to hide_body_button - setTimeout(() => { - var element = document.getElementById("hide_body_button") - if (element !== undefined && element !== null) { - // Keep the button a little below the top - element.scrollIntoView({ - behavior: "smooth", - block: "center", - }) - } - }, 100) - + } else { + //console.log("In found: ", found, hideBody) + } + } else { - }} - name="requires_unique" - /> - } - label={hideBody ? "Show Body" : "Hide Body"} - /> -
    -
    - ) + rows = "1"; + disabled = true; + openApiHelperText = "OpenAPI spec: fill the following fields."; - if (selectedApp.generated && data.name === "body") { - const regex = /\${(\w+)}/g; - const found = placeholder.match(regex); + var changed = false; + var tempArray = [] + for (let specKey in found) { + const tmpitem = found[specKey]; + var skip = false; - // setActivateHidingBodyButton(false) - // - hideBodyButton = hideBodyButtonValue; - if (found === null || !hideBody) { - if (found === null) { - setActivateHidingBodyButton(true); - } else { - //console.log("In found: ", found, hideBody) - } - } else { + for (let innerkey in selectedActionParameters) { + if (selectedActionParameters[innerkey].name === tmpitem) { + skip = true; + break; + } + } - rows = "1"; - disabled = true; - openApiHelperText = "OpenAPI spec: fill the following fields."; + if (skip) { + //console.log("SKIPPING ", tmpitem) + continue; + } - var changed = false; - var tempArray = [] - for (let specKey in found) { - const tmpitem = found[specKey]; - var skip = false; + changed = true; + var isRequired = false + // Check if original field name is in the selectedAction.required_body_fields + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null) { + for (let innerkey in selectedAction.required_body_fields) { + if (selectedAction.required_body_fields[innerkey] === tmpitem) { + isRequired = true + break + } + } + } - for (let innerkey in selectedActionParameters) { - if (selectedActionParameters[innerkey].name === tmpitem) { - skip = true; - break; - } - } + tempArray.push({ + action_field: "", + configuration: false, + description: openApiFieldDesc, + example: "", + id: "", + multiline: true, + name: tmpitem, + options: null, + required: isRequired, + schema: { type: "string" }, + skip_multicheck: false, + tags: null, + value: "", + variant: "STATIC_VALUE", + field_active: true, + + autocompleted: true, + }); + } + + var required = selectedActionParameters.filter(item => item.required === true) + var notRequired = selectedActionParameters.filter(item => item.required === false) - if (skip) { - //console.log("SKIPPING ", tmpitem) - continue; - } + if (tempArray.length > 0) { + // Sort tempArray based on tempArray.required + tempArray.sort((a, b) => (a.required < b.required) ? 1 : -1) + // Add all items to the selectedActionParameters array + for (let innerkey in tempArray) { + tempArray[innerkey].id = "ADDED" - changed = true; - var isRequired = false - // Check if original field name is in the selectedAction.required_body_fields - if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null) { - for (let innerkey in selectedAction.required_body_fields) { - if (selectedAction.required_body_fields[innerkey] === tmpitem) { - isRequired = true - break - } - } - } + if (tempArray[innerkey].required === true) { + required.push(tempArray[innerkey]) + } else { + notRequired.push(tempArray[innerkey]) + } + } + } + //selectedActionParameters + if (changed) { + // Sort selectedActionParameters based on selectedActionParameters.required + //selectedActionParameters.sort((a, b) => (a.required < b.required) ? 1 : -1) + // Find the "headers" and "queries" field names and put them on the first indexes anyway + var newArray = required.concat(notRequired) + - tempArray.push({ - action_field: "", - configuration: false, - description: openApiFieldDesc, - example: "", - id: "", - multiline: true, - name: tmpitem, - options: null, - required: isRequired, - schema: { type: "string" }, - skip_multicheck: false, - tags: null, - value: "", - variant: "STATIC_VALUE", - field_active: true, - - autocompleted: true, - }); - } - - console.log("TEMP ARRAY: ", tempArray) - var required = selectedActionParameters.filter(item => item.required === true) - var notRequired = selectedActionParameters.filter(item => item.required === false) + setSelectedActionParameters(newArray) + } - if (tempArray.length > 0) { - // Sort tempArray based on tempArray.required - tempArray.sort((a, b) => (a.required < b.required) ? 1 : -1) - // Add all items to the selectedActionParameters array - for (let innerkey in tempArray) { - tempArray[innerkey].id = "ADDED" + return hideBodyButton; + } + } - if (tempArray[innerkey].required === true) { - required.push(tempArray[innerkey]) - } else { - notRequired.push(tempArray[innerkey]) - } - } - } - //selectedActionParameters - - if (changed) { - // Sort selectedActionParameters based on selectedActionParameters.required - //selectedActionParameters.sort((a, b) => (a.required < b.required) ? 1 : -1) - // Find the "headers" and "queries" field names and put them on the first indexes anyway - var newArray = required.concat(notRequired) - - - setSelectedActionParameters(newArray) - } - - return hideBodyButton; - } - } - - if (activateHidingBodyButton === true) { - hideBodyButton = ""; - } + if (activateHidingBodyButton === true) { + hideBodyButton = ""; + } const clickedFieldId = "rightside_field_" + count; @@ -1862,7 +3105,7 @@ const ParsedAction = (props) => { }} /> - + { @@ -1899,8 +3142,12 @@ const ParsedAction = (props) => { id={clickedFieldId} rows={data.name.startsWith("${") && data.name.endsWith("}") ? 2 : rows} color="primary" - defaultValue={data.value} - //value={data.value} + // defaultValue={data.value} + value={ + paramValues.find((param) => param.name === data.name) !== undefined + ? paramValues.find((param) => param.name === data.name).value + : "" + } //options={{ // theme: 'gruvbox-dark', // keyMap: 'sublime', @@ -1920,7 +3167,8 @@ const ParsedAction = (props) => { placeholder={placeholder} onChange={(event) => { //changeActionParameterCodemirror(event, count, data) - changeActionParameter(event, count, data); + // changeActionParameter(event, count, data); + handleParamChange(event, count, data) }} helperText={returnHelperText(data.name, data.value)} onBlur={(event) => { @@ -2315,7 +3563,6 @@ const ParsedAction = (props) => { }; const handleItemClick = (values) => { - console.log("In normal itemclick") if (values === undefined ||values === null ||values.length === 0) { return; } @@ -2338,31 +3585,28 @@ const ParsedAction = (props) => { // Handles the fields under OpenAPI body to be parsed. if (data.name.startsWith("${") && data.name.endsWith("}")) { - console.log("INSIDE VALUE REPLACE: ", data.name, toComplete); - // PARAM FIX - Gonna use the ID field, even though it's a hack const paramcheck = selectedAction.parameters.find( (param) => param.name === "body" - ); + ) + if (paramcheck !== undefined) { - if ( - paramcheck["value_replace"] === undefined || - paramcheck["value_replace"] === null - ) { + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { paramcheck["value_replace"] = [ { key: data.name, value: toComplete, }, - ]; + ] } else { - const subparamindex = paramcheck[ - "value_replace" - ].findIndex((param) => param.key === data.name); + const subparamindex = paramcheck["value_replace"] + .findIndex((param) => param.key === data.name); + if (subparamindex === -1) { paramcheck["value_replace"].push({ key: data.name, value: toComplete, - }); + }) + } else { paramcheck["value_replace"][subparamindex]["value"] += toComplete; @@ -2370,7 +3614,9 @@ const ParsedAction = (props) => { } selectedActionParameters[count]["value_replace"] = paramcheck; - selectedAction.parameters[count]["value_replace"] = paramcheck; + + selectedAction.parameters = selectedActionParameters + //selectedAction.parameters[count]["value_replace"] = paramcheck; setSelectedAction(selectedAction); setUpdate(Math.random()); @@ -2387,7 +3633,7 @@ const ParsedAction = (props) => { //selectedAction.parameters[count].value = selectedActionParameters[count].value; //setSelectedAction(selectedAction); //setUpdate(Math.random()); - + setShowDropdown(false); setMenuPosition(null); }; @@ -2613,7 +3859,8 @@ const ParsedAction = (props) => { newname = newname.slice(0, newname.length-5) } - selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` + //selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` + selectedActionParameters[count].value += `$${innerdata.name}.${newname}` selectedAction.parameters[count].value = selectedActionParameters[count].value; setSelectedAction(selectedAction); setUpdate(Math.random()); @@ -2690,10 +3937,16 @@ const ParsedAction = (props) => { } const buttonTitle = `Authenticate ${selectedApp.name.replaceAll("_", " ")}` - const hasAutocomplete = data.autocompleted === true + const hasAutocomplete = data?.autocompleted === true + + + if (data.variant === undefined || data.variant === null) { + data.variant = "STATIC_VALUE" + } + return (
    - {hideBodyButton} + {/* {hideBodyButton} */}
    @@ -2827,9 +4080,9 @@ const ParsedAction = (props) => { Autocomplete { - const newversion = selectedApp.versions.find( - (tmpApp) => tmpApp.version == event.target.value - ) - - if (newversion !== undefined && newversion !== null) { - getApp(newversion.id, true) - } - - // Change in all actions in the workflow at the same time and add a toast.success() about it - for (var actionkey in workflow.actions) { - const action = workflow.actions[actionkey] - if (action.app_name === selectedAction.app_name) { - workflow.actions[actionkey].app_version = event.target.value - } - } - - toast.success("Changed version of all nodes to "+event.target.value) - }} - style={{ - marginTop: 10, - backgroundColor: theme.palette.surfaceColor, - backgroundColor: theme.palette.inputColor, - color: "white", - height: 35, - marginleft: 10, - borderRadius: theme.palette.borderRadius, - }} - SelectDisplayProps={{ - style: { - }, - }} - > - {selectedApp.versions.map((data, index) => { - return ( - - {data.version} - - ); - })} - - ) : null} -
    -
    -
    -
    - Name - { - // Copy the name value - const name = e.target.value - const parsedBaseLabel = "$"+baselabel.toLowerCase().replaceAll(" ", "_") - const newname = "$"+name.toLowerCase().replaceAll(" ", "_") - - // Check if it's the same as the current name in use - //if (name === selectedAction.label) { - // console.log("Returning from name thing") - // return - //} - - // Change in actions, triggers & conditions - // Highlight the changes somehow with a glow? - if (workflow.branches !== undefined && workflow.branches !== null) { - for (let [key,keyval] in Object.entries(workflow.branches)) { - if (workflow.branches[key].conditions !== undefined && workflow.branches[key].conditions !== null) { - for (let [subkey,subkeyval] in Object.entries(workflow.branches[key].conditions)) { - const condition = workflow.branches[key].conditions[subkey] - const sourceparam = condition.source - const destinationparam = condition.destination - - // Should have a smarter way of discovering node names - // Finding index(es) and replacing at the location - if (sourceparam.value.includes("$")) { - try { - var cnt = -1 - var previous = 0 - while (true) { - cnt += 1 - // Need to make sure e.g. changing the first here doesn't change the 2nd - // $change_me - // $change_me_2 - - const foundindex = sourceparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) - if (foundindex === previous && foundindex !== 0) { - break - } - - if (foundindex >= 0) { - previous = foundindex+newname.length - // Need to add diff of length to word - - // Check location: - // If it's a-zA-Z_ then don't replace - if (sourceparam.value.length > foundindex+parsedBaseLabel.length) { - const regex = /[a-zA-Z0-9_]/g; - const match = sourceparam.value[foundindex+parsedBaseLabel.length].match(regex); - if (match !== null) { - continue - } - } - - console.log("Old found: ", workflow.branches[key].conditions[subkey].source.value) - const extralength = newname.length-parsedBaseLabel.length - sourceparam.value = sourceparam.value.substring(0, foundindex) + newname + sourceparam.value.substring(foundindex-extralength+newname.length, sourceparam.value.length) - - console.log("New: ", workflow.branches[key].conditions[subkey].source.value) - } else { - break - } - - // Break no matter what after 5 replaces. May need to increase - if (cnt >= 5) { - break - } - - } - } catch (e) { - console.log("Failed value replacement based on index: ", e) - } - } - - if (destinationparam.value.includes("$")) { - try { - var cnt = -1 - var previous = 0 - while (true) { - cnt += 1 - // Need to make sure e.g. changing the first here doesn't change the 2nd - // $change_me - // $change_me_2 - - const foundindex = destinationparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) - if (foundindex === previous && foundindex !== 0) { - break - } - - if (foundindex >= 0) { - previous = foundindex+newname.length - // Need to add diff of length to word - - // Check location: - // If it's a-zA-Z_ then don't replace - if (destinationparam.value.length > foundindex+parsedBaseLabel.length) { - const regex = /[a-zA-Z0-9_]/g; - const match = destinationparam.value[foundindex+parsedBaseLabel.length].match(regex); - if (match !== null) { - continue - } - } - - console.log("Old found: ", workflow.branches[key].conditions[subkey].destination.value) - const extralength = newname.length-parsedBaseLabel.length - destinationparam.value = destinationparam.value.substring(0, foundindex) + newname + destinationparam.value.substring(foundindex-extralength+newname.length, destinationparam.value.length) - - console.log("New: ", workflow.branches[key].conditions[subkey].destination.value) - } else { - break - } - - // Break no matter what after 5 replaces. May need to increase - if (cnt >= 5) { - break - } - - } - } catch (e) { - console.log("Failed value replacement based on index: ", e) - } - } - } - } - } - } - - for (let [key,keyval] in Object.entries(workflow.actions)) { - if (workflow.actions[key].id === selectedAction.id) { - continue - } - - const params = workflow.actions[key].parameters - console.log(params) - if (params === null || params === undefined) { - continue - } - - for (let [subkey, subkeyval] in Object.entries(params)) { - const param = workflow.actions[key].parameters[subkey]; - if (!param.value.includes("$")) { - continue - } - - // Should have a smarter way of discovering node names - // Do regex? - // Finding index(es) and replacing at the location - // - - try { - var cnt = -1 - var previous = 0 - while (true) { - cnt += 1 - // Need to make sure e.g. changing the first here doesn't change the 2nd - // $change_me - // $change_me_2 - - const foundindex = param.value.toLowerCase().indexOf(parsedBaseLabel, previous) - if (foundindex === previous && foundindex !== 0) { - break - } - - if (foundindex >= 0) { - previous = foundindex+newname.length - // Need to add diff of length to word - - // Check location: - // If it's a-zA-Z_ then don't replace - if (param.value.length > foundindex+parsedBaseLabel.length) { - const regex = /[a-zA-Z0-9_]/g; - const match = param.value[foundindex+parsedBaseLabel.length].match(regex); - if (match !== null) { - continue - } - } - - console.log("Old found: ", workflow.actions[key].parameters[subkey].value) - const extralength = newname.length-parsedBaseLabel.length - param.value = param.value.substring(0, foundindex) + newname + param.value.substring(foundindex-extralength+newname.length, param.value.length) - - console.log("New: ", workflow.actions[key].parameters[subkey].value) - } else { - break - } - - // Break no matter what after 5 replaces. May need to increase - if (cnt >= 5) { - break - } - - } - } catch (e) { - console.log("Failed value replacement based on index: ", e) - } - } - } - - setWorkflow(workflow); - setUpdate(Math.random()); - baselabel = name - }} - /> -
    - {/*!isCloud ? null :*/} -
    - - - Delay - { - if (actionDelayChange !== undefined) { - actionDelayChange(event) - } - }} - /> - - -
    - {/**/} -
    - - )} - {selectedApp.name !== undefined && - selectedAction.authentication !== null && - selectedAction.authentication !== undefined && - selectedAction.authentication.length === 0 && - requiresAuthentication ? ( -
    - - - - - -
    - ) : null} - - {selectedAction.authentication !== undefined && - selectedAction.authentication !== null && - selectedAction.authentication.length > 0 ? ( -
    - Authentication -
    - - - {/* - - - curaction.authentication = authenticationOptions - if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") - */} - - { - setAuthenticationModalOpen(true); - }} - > - - - -
    -
    - ) : null} - - {showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? ( -
    - Environment - -
    - ) : null} - - {workflow.execution_variables !== undefined && - workflow.execution_variables !== null && - workflow.execution_variables.length > 0 ? ( -
    - Execution variable (optional) - -
    - ) : null} - - -
    - {/*hideExtraTypes ? null : -
    - Actions -
    - */} - - {setNewSelectedAction !== undefined ? ( - { - // Most popular - // Is categorized - // Uncategorized - return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; - }} - renderGroup={(params) => { - - return ( -
  • - {params.group} - {params.children} -
  • - ) - }} - options={renderedActionOptions} - ListboxProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - }, - }} - filterOptions={(options, { inputValue }) => { - //console.log("Option contains?: ", inputValue, options) - const lowercaseValue = inputValue.toLowerCase() - options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) - - return options - }} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.name === undefined || option.name === null ) { - return null; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - - return newname; - }} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette.borderRadius, - }} - onChange={(event, newValue) => { - // Workaround with event lol - if (newValue !== undefined && newValue !== null) { - setNewSelectedAction({ - target: { - value: newValue.name - } - }); - } - }} - renderOption={(props, data, state) => { - var newActionname = data.name; - if (data.label !== undefined && data.label !== null && data.label.length > 0) { - newActionname = data.label; - } - - var newActiondescription = data.description; - //console.log("DESC: ", newActiondescription) - if (data.description === undefined || data.description === null) { - newActiondescription = "Description: No description defined for this action" - } else { - newActiondescription = "Description: "+newActiondescription - } - - const iconInfo = GetIconInfo({ name: data.name }); - const useIcon = iconInfo.originalIcon; - - if (newActionname === undefined || newActionname === null) { - newActionname = "No name" - data.name = "No name" - data.label = "No name" - } - - newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); - - var method = "" - var extraDescription = "" - if (data.name.includes("get_")) { - method = "GET" - } else if (data.name.includes("post_")) { - method = "POST" - } else if (data.name.includes("put_")) { - method = "PUT" - } else if (data.name.includes("patch_")) { - method = "PATCH" - } else if (data.name.includes("delete_")) { - method = "DELETE" - } else if (data.name.includes("options_")) { - method = "OPTIONS" - } else if (data.name.includes("connect_")) { - method = "CONNECT" - } - - // FIXME: Should it require a base URL? - if (method.length > 0 && data.description !== undefined && data.description !== null && data.description.includes("http")) { - var extraUrl = "" - const descSplit = data.description.split("\n") - // Last line of descSplit - if (descSplit.length > 0) { - extraUrl = descSplit[descSplit.length-1] - } - - //for (let [line,lineval] in Object.entries(descSplit)) { - // if (descSplit[line].includes("http") && descSplit[line].includes("://")) { - // const urlsplit = descSplit[line].split("/") - // try { - // extraUrl = "/"+urlsplit.slice(3, urlsplit.length).join("/") - // } catch (e) { - // //console.log("Failed - running with -1") - // extraUrl = "/"+urlsplit.slice(3, urlsplit.length-1).join("/") - // } - - - // //console.log("NO BASEURL TOO!! Why missing last one in certain scenarios (sevco)?", extraUrl, urlsplit, descSplit[line]) - // //break - // } - //} - - if (extraUrl.length > 0) { - if (extraUrl.includes(" ")) { - extraUrl = extraUrl.split(" ")[0] - } - - if (extraUrl.includes("#")) { - extraUrl = extraUrl.split("#")[0] - } - - extraDescription = `${method} ${extraUrl}` - } else { - //console.log("No url found. Check again :)") - } - } - - return ( - - ); - }} - renderInput={(params) => { - if (params.inputProps !== undefined && params.inputProps !== null && params.inputProps.value !== undefined && params.inputProps.value !== null) { - const prefixes = ["Post", "Put", "Patch"] - for (let [key,keyval] in Object.entries(prefixes)) { - if (params.inputProps.value.startsWith(prefixes[key])) { - params.inputProps.value = params.inputProps.value.replace(prefixes[key]+" ", "", -1) - if (params.inputProps.value.length > 1) { - params.inputProps.value = params.inputProps.value.charAt(0).toUpperCase()+params.inputProps.value.substring(1) - } - break - } - } - - // Check if it starts with "Get List" and method is "Get" - if (params.inputProps.value.startsWith("Get List")) { - console.log("Get List") - } - } - - return ( - - ); - }} - /> - ) : null} - - {/*setNewSelectedAction !== undefined ? - - : null*/} - -
    - +
    diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx index 234269ae..289d2b7c 100644 --- a/frontend/src/components/Priorities.jsx +++ b/frontend/src/components/Priorities.jsx @@ -27,8 +27,9 @@ const Priorities = (props) => { const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT"); const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT"); - let navigate = useNavigate(); + const [highlightKMS, setHighlightKMS] = React.useState(false) + let navigate = useNavigate(); useEffect(() => { getFramework() @@ -36,6 +37,12 @@ const Priorities = (props) => { const urlParams = new URLSearchParams(window.location.search) const workflow = urlParams.get("workflow") const execution_id = urlParams.get("execution_id") + const kms = urlParams.get("kms") + + if (kms !== null && kms !== undefined && kms.length > 0 && kms === "true") { + toast.info("KMS-related notifications are highlighted.") + setHighlightKMS(true) + } if (execution_id !== null) { setSelectedExecutionId(execution_id) @@ -217,7 +224,16 @@ const Priorities = (props) => { var orgId = ""; - const highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) + var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) + + if (!highlighted && highlightKMS) { + if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) { + highlighted = true + } else if (data.description !== undefined && data.description !== null && data.description.toLowerCase().includes("kms")) { + highlighted = true + } + + } if (userdata.orgs !== undefined) { const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index 35e7b9e3..c390666a 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -88,12 +88,12 @@ const SearchData = props => { const textFieldRef = useRef(null); const keyPressHandler = (e) => { - if (e.which === 13) { + if (e.key === "Enter") { + e.preventDefault(); // navigate(`/search?q=${currentRefinement}`, { state: value, replace: true }); // setModalOpen(false); const trimmedValue = inputValue.trim(); if (trimmedValue !== '') { - e.preventDefault(); navigate(`/search?q=${trimmedValue}`, { state: trimmedValue, replace: true }); setModalOpen(false); } diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index a23de3e5..1dc0ecaa 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -107,10 +107,9 @@ const CodeEditor = (props) => { getParents, fieldname, + contentLoading, } = props - - const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata); //const { setContainer } = useCodeMirror({ @@ -627,8 +626,8 @@ const CodeEditor = (props) => { newMarkers.push({ startRow: i, startCol: startCh, - endRow: i+1, - endCol: endCh+1, + endRow: i, + endCol: endCh, className: correctVariable ? "good-marker" : "bad-marker", type: "text", }) @@ -989,6 +988,17 @@ const CodeEditor = (props) => { }, }} > + + {contentLoading === true ? + + + + : null} + { >
    - Expected Output + + {selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Code to run" : "Expected Output"}
    @@ -1588,7 +1599,7 @@ const CodeEditor = (props) => { border: `1px solid ${theme.palette.primary.main}`, position: "absolute", top: 24, - right: 65, + right: 100, maxHeight: 35, minWidth: 70, }} @@ -1599,7 +1610,7 @@ const CodeEditor = (props) => { {executing ? : - Try it + {selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Run Python Code" : "Try it"} }
    diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 589e4f7d..791b8835 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -204,6 +204,9 @@ const AppGrid = props => { style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}} InputProps={{ style:{ + color: "white", + fontSize: "1em", + height: 50, }, startAdornment: ( @@ -221,6 +224,11 @@ const AppGrid = props => { removeQuery("q") refine(event.currentTarget.value) }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} limit={5} /> : null} @@ -233,6 +241,8 @@ const AppGrid = props => { flexWrap: "wrap", alignContent: "space-between", marginTop: 5, + padding: "0px 180px", + width:"auto" } var workflowDelay = -50 diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index d36c8e8d..6f2455b3 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -133,6 +133,11 @@ const data = [ "background-gradient-stop-colors": "data(fillGradient)", }, }, + { + selector: `node[?parent_controlled]`, + css: { + }, + }, { selector: `node[app_name="Testing"]`, css: { diff --git a/frontend/src/index.css b/frontend/src/index.css index 9b1fc96c..3ab58ef1 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -18,11 +18,9 @@ code { margin-right: -13px; } -.toc:hover { - background-color: gray; - color: white; +table th, table td { + border: 1px solid; } - /* .cm-string{ z-index: -1; } diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index b074e404..15382695 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -44,6 +44,7 @@ import { Box, InputAdornment, Autocomplete, + Modal } from "@mui/material"; import { @@ -75,6 +76,7 @@ import { Help as HelpIcon, Flag as FlagIcon, FmdGood as FmdGoodIcon, + Warning as WarningIcon, } from "@mui/icons-material"; //import { useAlert @@ -164,9 +166,11 @@ const Admin = (props) => { const [selectedOrganization, setSelectedOrganization] = React.useState({}); //console.log("Selected: ", selectedOrganization) - const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false); + const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false); const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); + const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState(""); const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [organizationFeatures, setOrganizationFeatures] = React.useState({}); @@ -210,6 +214,13 @@ const Admin = (props) => { const [allSchedules, setAllSchedules] = React.useState([]); const [pipelines, setPipelines] = React.useState([]); const [, forceUpdate] = React.useState(); + const [MFARequired, setMFARequired] = React.useState(selectedOrganization.mfa_required === undefined ? false : selectedOrganization.mfa_required); + + useEffect(() => { + if (selectedOrganization.mfa_required !== undefined) { + setMFARequired(selectedOrganization.mfa_required); + } + }, [selectedOrganization.mfa_required]); const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] = React.useState(false); @@ -431,23 +442,62 @@ const Admin = (props) => { }); }; - const createAppAuthenticationGroup = (name, description, appAuthIds) => { + const deleteAppAuthenticationGroup = (appAuthGroupId) => { + const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}` + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for deleting app auth group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to delete app authentication group"); + } else { + toast("App authentication group deleted") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()) + }) + } + + const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => { + // Makes list of ids into a full-on list of auth, but just with the ID + // The backend fills in the rest + console.log("INput auth: ", appAuthIds) let app_auths = appAuthIds.map((appAuthId) => { return { id: appAuthId }; }) - fetch(globalUrl + "/api/v1/apps/authentication/group", { + var parsedAppGroup = { + label: name, + environment: environment, + description: description, + app_auths: app_auths + } + + if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") { + parsedAppGroup.id = appAuthenticationGroupId + } + + fetch(globalUrl + "/api/v1/authentication/group", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", - body: JSON.stringify({ - label: name, - description: description, - app_auths: app_auths - }), + body: JSON.stringify(parsedAppGroup), }) .then((response) => { if (response.status !== 200) { @@ -457,8 +507,15 @@ const Admin = (props) => { return response.json(); }) .then((responseJson) => { - // getAppAuthenticationGroups(); - toast("App authentication group created"); + if (responseJson.success === false) { + toast("Failed to create. Please try again, or contact support@shuffler.io") + } else { + // Close the modal + setAppAuthenticationGroupModalOpen(false) + + toast("App authentication group created") + getAppAuthenticationGroups() + } }) .catch((error) => { toast(error.toString()); @@ -805,7 +862,7 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined setAllSchedules(responseJson.schedules || []); - setPipelines(responseJson.pipelines || []); + // setPipelines(responseJson.pipelines || []); }) .catch((error) => { // toast(error.toString()); @@ -990,13 +1047,11 @@ If you're interested, please let me know a time that works for you, or set up a } const data = { - command: pipeline.command, name: pipeline.name, type: state, environment: pipeline.environment, workflow_id: pipeline.workflow_id, trigger_id: pipeline.trigger_id, - start_node: pipeline.start_node, }; if (state === "start") toast("starting the pipeline"); @@ -1027,7 +1082,6 @@ If you're interested, please let me know a time that works for you, or set up a if (state === "start") toast("Successfully created pipeline"); else toast("Sucessfully stopped the pipeline"); } - setTimeout(handleGetAllTriggers, 1000); }) .catch((error) => { //toast(error.toString()); @@ -1052,6 +1106,7 @@ If you're interested, please let me know a time that works for you, or set up a code: code, user_id: userId, }; + toast("Verifying 2fa code. Please wait..."); fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, { mode: "cors", @@ -1075,11 +1130,15 @@ If you're interested, please let me know a time that works for you, or set up a }) .then((responseJson) => { if (responseJson.success === true) { - toast("Successfully enabled 2fa"); + if (responseJson.MFAActive === true) { + toast.success("Successfully enabled 2fa"); + } + if (responseJson.MFAActive === false) { + toast.success("Successfully disabled 2fa"); + } setTimeout(() => { getUsers(); - setImage2FA(""); setValue2FA(""); setSecret2FA(""); @@ -1510,6 +1569,11 @@ If you're interested, please let me know a time that works for you, or set up a responseJson.lead_info !== null ) { var leads = []; + console.log("LEADINFO: ", responseJson.lead_info) + if (responseJson.lead_info.testing_shuffle) { + leads.push("testing shuffle"); + } + if (responseJson.lead_info.contacted) { leads.push("contacted"); } @@ -2093,10 +2157,10 @@ If you're interested, please let me know a time that works for you, or set up a }; const getAppAuthenticationGroups = () => { - console.log("DEBUG: Skipping app auth group loading") - return + //console.log("DEBUG: Skipping app auth group loading") + //return - fetch(globalUrl + "/api/v1/apps/authentication/group", { + fetch(globalUrl + "/api/v1/authentication/group", { method: "GET", headers: { "Content-Type": "application/json", @@ -2877,8 +2941,8 @@ If you're interested, please let me know a time that works for you, or set up a color="primary" > {selectedUser.mfa_info !== undefined && - selectedUser.mfa_info !== null && - selectedUser.mfa_info.active === true + selectedUser.mfa_info !== null && + selectedUser.mfa_info.active === true ? "Disable 2FA" : "Enable 2FA"} @@ -2964,15 +3028,16 @@ If you're interested, please let me know a time that works for you, or set up a maxWidth: 300, minWidth: 300, marginTop: 25, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', }} > - {/**/} - {secret2FA !== undefined && - secret2FA !== null && - secret2FA.length > 0 ? ( + secret2FA !== null && + secret2FA.length > 0 ? ( - + Scan the image below with the two-factor authentication app on your phone. If you can’t use a QR code, use the code{" "} {secret2FA} instead. @@ -2980,18 +3045,16 @@ If you're interested, please let me know a time that works for you, or set up a ) : null} {image2FA !== undefined && - image2FA !== null && - image2FA.length > 0 ? ( + image2FA !== null && + image2FA.length > 0 ? ( {"2 ) : ( @@ -3002,7 +3065,7 @@ If you're interested, please let me know a time that works for you, or set up a After scanning the QR code image, the app will display a code that you can enter below. -
    +
    { + if (event.key === 'Enter' && event.target.value.length === 6) { + handleVerify2FA(userdata.id, event.target.value, false); + } + }} />
    + } + style={{ + minWidth: 225, + maxWidth: 225, + overflow: "hidden", + }} + /> + + {/* + + */} + + {/* + + */} + { + return data.key; + }) + .join(", ") + } + style={{ + minWidth: 140, + maxWidth: 140, + overflow: "auto", + marginRight: 10, + }} + /> + + + { + updateAppAuthentication(data); + }} + disabled={ + data.org_id !== selectedOrganization.id ? true : false + } + > + + + {data.defined ? ( + + { + editAuthenticationConfig(data.id); + }} + > + + + + ) : ( + + {}} + disabled={ + data.org_id !== selectedOrganization.id + ? true + : false + } + > + + + + )} + { + deleteAuthentication(data); + }} + > + + + + + {selectedOrganization.id !== undefined && + data.org_id !== selectedOrganization.id ? ( + + + + ) : ( + + { + changeDistribution(data, !isDistributed); + }} + /> + + )} + + + ) + } const authenticationView = curTab === 2 ? ( - <> + +
    {/* (appAuthenticationGroupModalOpen : { */} {appAuthenticationGroupModalOpen && ( { setAppAuthenticationGroupModalOpen(false); + + setAppAuthenticationGroupId("") + setAppAuthenticationGroupName("") + setAppAuthenticationGroupEnvironment("") + setAppAuthenticationGroupDescription("") + setAppsForAppAuthGroup([]) }} PaperProps={{ style: { backgroundColor: theme.palette.surfaceColor, color: "white", - minWidth: "1200px", + minWidth: "1000px", minHeight: "320px", + padding: 25, + paddingLeft: 50, }, }} > @@ -5202,106 +5673,134 @@ If you're interested, please let me know a time that works for you, or set up a App Authentication Groups - -
    - { - setAppAuthenticationGroupName(event.target.value); - }} - /> + +
    +
    + + Name + + { + setAppAuthenticationGroupName(event.target.value); + }} + /> +
    +
    + + Evironment + + {environments !== undefined && environments !== null && environments.length > 0 ? + + : + + Environments failed to load. Please try again + + }
    -
    - { - setAppAuthenticationGroupDescription(event.target.value); - }} - /> -
    - -
    - {/* Show a check box list of all app authentications to add to the auth group */} -
    - {authentication.map((data, index) => ( -
    - -
    - - { - handleAppAuthGroupCheckbox(data) - }} - name={data.label} - disabled={data.app.id in appsForAppAuthGroup} - /> -
    - - } - label={data.label} - /> -
    - ))} -
    - -
    - - -
    +
    +
    + + + +
    + {/* Show a check box list of all app authentications to add to the auth group */} +
    + {authentication.map((data, index) => { + var checked = data.checked + if (data.label !== undefined && data.label !== null && data.label.toLowerCase() === "kms shuffle storage") { + return null + } + + if (checked === undefined || checked === null) { + checked = false + } + + if (appsForAppAuthGroup.includes(data.id)) { + checked = true + } + + return ( +
    + +
    + + { + handleAppAuthGroupCheckbox(data) + }} + name={data.label} + disabled={data.app.id in appsForAppAuthGroup} + /> +
    + + } + label={data.label} + /> +
    + ) + })} +
    +
    + +
    )} @@ -5311,7 +5810,7 @@ If you're interested, please let me know a time that works for you, or set up a

    App Authentication

    - Control the authentication options for individual apps. + Control the authentication options for individual apps. App Groups are farther down on this page.   + { - var bgColor = "#27292d"; - if (index % 2 === 0) { - bgColor = "#1f2023"; - } - //console.log("Auth data: ", data) if (data.type === "oauth2") { data.fields = [ @@ -5402,195 +5900,30 @@ If you're interested, please let me know a time that works for you, or set up a ]; } - const isDistributed = - data.suborg_distributed === true ? true : false; - return ( - - - style={{ minWidth: 75, maxWidth: 75 }} - /> - - - {/* - - */} - - {/* - - */} - { - return data.key; - }) - .join(", ") - } - style={{ - minWidth: 125, - maxWidth: 125, - overflow: "auto", - marginRight: 10, - }} - /> - - - { - updateAppAuthentication(data); - }} - disabled={ - data.org_id !== selectedOrganization.id ? true : false - } - > - - - {data.defined ? ( - - { - editAuthenticationConfig(data.id); - }} - > - - - - ) : ( - - {}} - disabled={ - data.org_id !== selectedOrganization.id - ? true - : false - } - > - - - - )} - { - deleteAuthentication(data); - }} - > - - - - - {selectedOrganization.id !== undefined && - data.org_id !== selectedOrganization.id ? ( - - - - ) : ( - - { - changeDistribution(data, !isDistributed); - }} - /> - - )} - - - ); + + ) + + })}
    - {/*
    + + +
    -

    App Authentication Groups

    - +

    App Authentication Groups

    + Groups of authentication options for subflows.{" "}
    - + + + + - - {data.app_auths.map((appAuth, index) => ( - - {appAuth.app.name} - - ))} + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })}
    } style={{ minWidth: 250, maxWidth: 250 }} @@ -5676,16 +6037,22 @@ If you're interested, please let me know a time that works for you, or set up a
    { + setAppAuthenticationGroupId(data.id) + + setAppAuthenticationGroupName(data.label) + setAppAuthenticationGroupDescription(data.description) + + setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id)) + setAppAuthenticationGroupEnvironment(data.environment) + setAppAuthenticationGroupModalOpen(true) }} - disabled={true} > { - // deleteAppAuthenticationGroup(data); + deleteAppAuthenticationGroup(data.id) }} - disabled={true} > @@ -5700,20 +6067,10 @@ If you're interested, please let me know a time that works for you, or set up a )} -
    -
    */} - +
    +
    ) : null; const getLogs = async (ip, userId) => { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 492e8ad9..889a407b 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-multi-comp */ -import React, { useState, useEffect, useLayoutEffect } from "react"; +import React, { useState, useEffect, useLayoutEffect, memo, useMemo, useRef } from "react"; import ReactDOM from "react-dom" import theme from "../theme.jsx"; @@ -22,7 +22,6 @@ 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, @@ -88,6 +87,7 @@ import { Error as ErrorIcon, Warning as WarningIcon, ArrowLeft as ArrowLeftIcon, + ArrowRight as ArrowRightIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, FormatListNumbered as FormatListNumberedIcon, @@ -123,6 +123,8 @@ import { Add as AddIcon, ErrorOutline as ErrorOutlineIcon, + ArrowForward as ArrowForwardIcon, + } from "@mui/icons-material"; //import * as cytoscape from "cytoscape"; @@ -378,7 +380,7 @@ const svgSize = 24; const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") const AngularWorkflow = (defaultprops) => { - const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id } = defaultprops; + const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; const referenceUrl = globalUrl + "/api/v1/hooks/"; //const alert = useAlert() let navigate = useNavigate(); @@ -430,7 +432,7 @@ const AngularWorkflow = (defaultprops) => { 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) @@ -451,6 +453,9 @@ const AngularWorkflow = (defaultprops) => { const [lastExecution, setLastExecution] = React.useState(""); const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = 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; @@ -467,7 +472,6 @@ const AngularWorkflow = (defaultprops) => { const [conditionValue, setConditionValue] = React.useState({}); const [dragging, setDragging] = React.useState(false); const [showWorkflowRevisions, setShowWorkflowRevisions] = React.useState(false); - const [selectedRevision, setSelectedRevision] = useState({}) const [dragPosition, setDragPosition] = React.useState({ x: 0, y: 0, @@ -496,6 +500,7 @@ const AngularWorkflow = (defaultprops) => { 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) @@ -524,12 +529,13 @@ const AngularWorkflow = (defaultprops) => { 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 [distributedFromParent, setDistributedFromParent] = React.useState("") + const [suborgWorkflows, setSuborgWorkflows] = React.useState([]) + const [suggestionBox, setSuggestionBox] = React.useState({ "position": { "top": 500, @@ -539,8 +545,14 @@ const AngularWorkflow = (defaultprops) => { "attachedTo": "", }) + useEffect(() => { + if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { + saveWorkflow(workflow) + } + }, [editWorkflowModalOpen]) + // New for generated stuff - const releaseToConnectLabel = "Release to Connect" +const releaseToConnectLabel = "Release to Connect" const integrationApps = [{ "id": "integration", "name": "Integration Framework", @@ -828,6 +840,75 @@ const AngularWorkflow = (defaultprops) => { loopRunning2 = true } + useEffect(() => { + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id) { + 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: false, + 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 @@ -871,6 +952,10 @@ const AngularWorkflow = (defaultprops) => { }) .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) { // Translate into markdown ![]() @@ -958,7 +1043,7 @@ const AngularWorkflow = (defaultprops) => { }; const getAvailableWorkflows = (trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { + fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", headers: { "Content-Type": "application/json", @@ -987,22 +1072,20 @@ const AngularWorkflow = (defaultprops) => { // User Input & Subflow nodes if (param.name === "workflow" || param.name === "subflow") { - const paramIndex = param.name === "workflow" ? 0 : 5 - console.log("Current vs new: ", workflow.triggers[trigger_index].parameters[paramIndex].value, subworkflow.id) - - 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); - } - } - } - } + 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) { @@ -1307,7 +1390,7 @@ const AngularWorkflow = (defaultprops) => { if (response.status !== 200) { stop(); setExecutionModalView(0); - toast("Failed loading the workflow run") + //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; @@ -1360,18 +1443,18 @@ const AngularWorkflow = (defaultprops) => { trigger.parameters = [] - const topic = document.getElementById('topic')?.value - const bootstrapServers = document.getElementById('bootstrap_servers')?.value - const groupId = document.getElementById('group_id')?.value + const topic = document.getElementById('topic')?.value; + const bootstrapServers = document.getElementById('bootstrap_servers')?.value; + const groupId = document.getElementById('group_id')?.value; //const autoOffsetReset = document.getElementById('auto_offset_reset')?.value; if(topic) { trigger.parameters.push({ name: "topic", value: topic - }) + }); } else { - toast("Please enter the topic name"); + toast("please enter the topic name"); return; } @@ -1675,12 +1758,18 @@ const AngularWorkflow = (defaultprops) => { }) } - const saveWorkflow = (curworkflow, executionArgument, startNode) => { + const saveWorkflow = (curworkflow, executionArgument, startNode, duplicationOrg) => { var success = false; if (isCloud && !isLoggedIn) { console.log("Should redirect to register with redirect.") - window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + + 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 } @@ -1910,13 +1999,26 @@ const AngularWorkflow = (defaultprops) => { 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: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, body: JSON.stringify(useworkflow), credentials: "include", }) @@ -1924,11 +2026,21 @@ const AngularWorkflow = (defaultprops) => { setSavingState(0); 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"); executeWorkflow(executionArgument, startNode, true); @@ -1985,16 +2097,20 @@ const AngularWorkflow = (defaultprops) => { setTimeout(() => { setSavingState(0); }, 1500); + getRevisionHistory(useworkflow.id) } }) .catch((error) => { setSavingState(0); - //toast(error.toString()); console.log("Save workflow error: ", error.toString()); + toast.warn("Failed to save the workflow. Is the network down?") }); - setOriginalWorkflow(useworkflow) - return success; + if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id) { + setOriginalWorkflow(useworkflow) + } + + return success }; const monitorUpdates = () => { @@ -2026,23 +2142,25 @@ const AngularWorkflow = (defaultprops) => { 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(() => { - 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; - } setVisited([]) setExecutionRequest({}) @@ -2050,51 +2168,68 @@ const AngularWorkflow = (defaultprops) => { // 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 && 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 - } + if (executionArgument === undefined || executionArgument === null || executionArgument.length === 0) - 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) { + 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 } - if (param.value.indexOf("$exec") !== -1) { - foundmissing = true + 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) { - break + //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 + } } } - if (foundmissing) { - //toast("This workflow contains a node that requires an execution argument. Please provide one.") - 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: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", body: JSON.stringify(data), } @@ -2167,6 +2302,37 @@ const AngularWorkflow = (defaultprops) => { // This can be used to only show prioritzed ones later // Right now, it can prioritize authenticated ones //"Testing", + // + // + + const getAuthGroups = () => { + fetch(globalUrl + "/api/v1/authentication/groups", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + 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) => { fetch(globalUrl + "/api/v1/apps/authentication", { @@ -2177,127 +2343,130 @@ const AngularWorkflow = (defaultprops) => { }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for app auth :O!"); + .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() + + 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; } - return response.json(); - }) - .then((responseJson) => { - var shouldClose = false - if (responseJson.success) { - var newauth = []; - for (let authkey in responseJson.data) { - if (responseJson.data[authkey].defined === false) { - continue; - } + item.fields = newfields; - newauth.push(responseJson.data[authkey]); - } + const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) + const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) + if (itemname === appname) { + authenticationOptions.push(item); - setAppAuthentication(newauth); + // Always becoming the last one + if (item.edited > latest) { + latest = item.edited; + selectedAction.selectedAuthentication = item; - 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) - } - } - - 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); + 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; + } } - }) - .catch((error) => { - setAppAuthentication([]); - //toast("Auth loading error: " + error.toString()); - console.log("AppAuth error: " + error.toString()); - }); + } else { + //console.log("Not newer: ", item.edited, " vs ", latest) + } + } else { + //console.log("Appname is wrong: ", appname, " vs ", itemname) + } + } + + 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 = () => { @@ -3082,6 +3251,39 @@ const AngularWorkflow = (defaultprops) => { return apps }; + const getChildWorkflows = (parentWorkflowId) => { + 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 + + 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", @@ -3103,19 +3305,19 @@ const AngularWorkflow = (defaultprops) => { // 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"); + 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(`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); - } + } } } @@ -3125,9 +3327,28 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { // Load as JSON + // + // //console.log("Got workflow TXT: ", responseText) //const responseJson = JSON.parse(responseText) //console.log("Got workflow JSON: ", responseJson) + 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) + } + + 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) { @@ -3350,7 +3571,7 @@ const AngularWorkflow = (defaultprops) => { cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); } else { - setOriginalWorkflow(responseJson); + setOriginalWorkflow(responseJson) setWorkflow(responseJson); setWorkflowDone(true); @@ -3581,12 +3802,11 @@ const AngularWorkflow = (defaultprops) => { const onNodeDragStop = (event, selectedAction) => { const nodedata = event.target.data(); if (nodedata.id === selectedAction.id) { - //console.log("Same node, return") + //console.log("Same node, return") return } if (nodedata.finished === false) { - //console.log("Node is not finished, return") return } @@ -3704,14 +3924,11 @@ const AngularWorkflow = (defaultprops) => { } if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - console.log("Found triggers. Add!") - 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())) - console.log("Matches: ", matchingExecutions.length) const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" const decoratorNode = { position: { @@ -3820,142 +4037,141 @@ const AngularWorkflow = (defaultprops) => { } if (nodedata.id === selectedAction.id) { - return + 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: [], - } - }) - } - } - } - } - + 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: [], + } + }) + } + } + } + } + if ( originalLocation.x === 0 && originalLocation.y === 0 && @@ -4013,11 +4229,11 @@ const AngularWorkflow = (defaultprops) => { } // Ensure it only happens once - document.removeEventListener("mousemove", onMouseUpdate, false) - } + document.removeEventListener("mousemove", onMouseUpdate, false); + }; - document.addEventListener("mousemove", onMouseUpdate, false) - } + document.addEventListener("mousemove", onMouseUpdate, false); + }; useBeforeunload(() => { @@ -4030,7 +4246,286 @@ const AngularWorkflow = (defaultprops) => { 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 = [] + if (inputAction !== undefined && inputAction !== null) { + const parents = getParents(inputAction) + + console.log("Parents: ", parents) + 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 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 + + if (!value.includes(inputAction.label)) { + conversationData.query = inputAction.label.replaceAll("_", " ") + } + } + + // Onprem not available yet (April 2023) + // Should: Make OpenAI work for them with their own key + //fetch("https://shuffler.io/api/v1/conversation", { + fetch(`${globalUrl}/api/v1/conversation`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(conversationData), + credentials: "include", + }) + .then((response) => { + if (setSuggestionLoading !== undefined) { + setSuggestionLoading(false) + } + + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + 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 paramkey in inputAction.parameters) { + const actionParam = inputAction.parameters[paramkey] + + if (actionParam.autocompleted === true) { + continue + } + + if (actionParam.configuration === true && actionParam.name !== "url") { + continue + } + + if (actionParam.value !== "" && actionParam.value !== actionParam.example) { + console.log("Skipping: ", actionParam) + continue + } + + for (let respParam of responseJson.parameters) { + if (respParam.name === actionParam.name) { + console.log("Found match for param: ", respParam) + + if (respParam.value === "") { + break + } + + changed = true + + inputAction.parameters[paramkey].autocompleted = true + inputAction.parameters[paramkey].value = respParam.value + break + } + } + } + + if (changed === true) { + console.log("Setting action! Force update pls :)") + setUpdate(Math.random()) + setSelectedAction(inputAction) + } + + return + } + + console.log("Suggestionbox location: ", suggestionBox) + + // Add action + 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) => { + if (setSuggestionLoading !== undefined) { + setSuggestionLoading(false) + } + + console.log("Conv response error: ", error); + }); + } + + // Nodeselectbatching: // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once @@ -4041,15 +4536,12 @@ const AngularWorkflow = (defaultprops) => { //const data = JSON.parse(JSON.stringify(event.target.data())) const data = event.target.data() - - console.log("NODE SELECT: ", data) - if (data.app_name === "Shuffle Workflow") { - console.log("Shuffle Workflow selected") - if ((data.parameters !== undefined) && (data.parameters.length > 0)) { - getWorkflowApps(data.parameters[0].value) + 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 @@ -4114,6 +4606,7 @@ const AngularWorkflow = (defaultprops) => { workflow.actions[foundindex].name = curaction.name setWorkflow(workflow) + console.log(workflow) } break } @@ -4220,7 +4713,7 @@ const AngularWorkflow = (defaultprops) => { return } - // Inject HTML at a fixed location + // Inject HTML at a fixed location? //const newHtml = "

    Do you want to add this suggestion?

    " // Find mouse cursor position on screen @@ -4415,13 +4908,24 @@ const AngularWorkflow = (defaultprops) => { return; } else if (data.isDescriptor) { - console.log("Can't select descriptor"); + // 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) } - event.target.unselect(); return; } @@ -4793,7 +5297,59 @@ const AngularWorkflow = (defaultprops) => { } setTimeout(() => { - setSelectedTriggerIndex(trigger_index); + 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 + } + } + } + } + + setSelectedTriggerIndex(trigger_index) setSelectedTrigger(data) setSelectedActionEnvironment(data.env) }, 25) @@ -5143,10 +5699,11 @@ const AngularWorkflow = (defaultprops) => { // Checks for errors in edges when they're added const onEdgeAdded = (event) => { - const edge = event.target.data() - //console.log("EDGE ADDED!: ", edge) + setLastSaved(false); + const edge = event.target.data(); + + //console.log("edge added: ", edge) if (edge.source === undefined && edge.target === undefined) { - console.log("Edge source and target is undefined") return } @@ -5160,7 +5717,6 @@ const AngularWorkflow = (defaultprops) => { 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") } else { //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") { @@ -5173,10 +5729,10 @@ const AngularWorkflow = (defaultprops) => { console.log("Node: ", targetedge) if (targetedge !== -1) { + event.target.remove() //console.log("Found branch already!") toast.error("Triggers can have exactly one target node") - event.target.remove() return @@ -5197,10 +5753,6 @@ const AngularWorkflow = (defaultprops) => { } } - if (edge.decorator === true) { - console.log("Doing nothing to branch because decorator") - return - } var targetnode = workflow.triggers.findIndex( (data) => data.id === edge.target @@ -5240,14 +5792,15 @@ const AngularWorkflow = (defaultprops) => { } } - if (eventTarget.data("isDescriptor") === true || eventTarget.data("type") === "COMMENT") { + if ( + eventTarget.data("isDescriptor") === true || + eventTarget.data("type") === "COMMENT" + ) { console.log("Removing because of descriptor or comment") - event.target.remove() - return + event.target.remove(); + return; } - - setLastSaved(false) targetnode = -1; // Check if: @@ -5255,51 +5808,38 @@ const AngularWorkflow = (defaultprops) => { // dest == dest && source == source // backend: check all children? to stop recursion var found = false; - const branches = cy.edges().jsons() - - const startNode = cy.nodes().jsons().find((node) => node.data.isStartNode === true) - var startnodeId = workflow.start - if (startNode !== undefined && startNode !== null) { - startnodeId = startNode.data.id - } - - //for (let branchkey in workflow.branches) { - for (let branchkey in branches) { - const branch = branches[branchkey].data - - //if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) { - if (branch.target === edge.source && branch.source === edge.target) { - toast("A branch in the opposite direction already exists") - event.target.remove() - found = true - break - - //} else if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { - } else if (branch.target === edge.target && branch.source === edge.source) { - - if (branch.conditions === undefined) { - // Edgehandles - } else { - console.log("Removing because the same branch already exists") - event.target.remove() - - found = true - break - } - } else if (edge.target === startnodeId) { - targetnode = workflow.triggers.findIndex((data) => data.id === edge.source) + 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; + } else if ( + workflow.branches[branchkey].destination_id === edge.target && + workflow.branches[branchkey].source_id === edge.source + ) { + //toast("That branch already exists"); + event.target.remove(); + found = true; + break; + } else 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 + toast("Can't make arrow to starting node"); + event.target.remove(); + break; } found = true; } - //} else if (edge.source === workflow.branches[branchkey].source_id) { - } else if (edge.source === branch.source) { + } else 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 @@ -5343,6 +5883,7 @@ const AngularWorkflow = (defaultprops) => { newdst !== null ) { const dstdata = RunAutocompleter(newdst.data()); + //console.log("DST Autocompleter: ", dstdata); } var newbranch = { @@ -5701,9 +6242,6 @@ const AngularWorkflow = (defaultprops) => { } setWorkflow(workflow); - //if (data.type === "TRIGGER") { - // saveWorkflow(workflow); - //} } //var previouskey = 0 @@ -5983,7 +6521,15 @@ const AngularWorkflow = (defaultprops) => { workflow.id !== null && workflow.id.length > 0 ) { - window.location.pathname = "/workflows/" + props.match.params.key; + + // 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; @@ -6171,7 +6717,6 @@ const AngularWorkflow = (defaultprops) => { }; const addActionSuggestions = (nodedata, event) => { - console.log("App Action suggestions being added") if (nodedata.type !== "ACTION") { return } @@ -6187,10 +6732,37 @@ const AngularWorkflow = (defaultprops) => { const parentlabel = parentNode.data("label").toLowerCase().replace(" ", "_") const parentname = parentNode.data("app_name").toLowerCase().replace(" ", "_") if (!parentlabel.startsWith(parentname)) { - console.log("Bad startname to start with: ", parentname, parentlabel) 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) { + return + } + + break + } + + //const parentAction = parentNode.data("name") + 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, @@ -6204,8 +6776,7 @@ const AngularWorkflow = (defaultprops) => { // 2. Loop the apps' actions // 3. Find actions based on category label IF it exists - console.log("Fidning app match for: ", parentname) - var added = 0 + var addedLabels = [] for (let appKey in apps) { const curapp = apps[appKey] if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { @@ -6216,7 +6787,6 @@ const AngularWorkflow = (defaultprops) => { continue } - console.log("Found matching: ", curapp.name, parentname, curapp.actions.length) for (let actionKey in curapp.actions) { const curaction = curapp.actions[actionKey] @@ -6226,7 +6796,13 @@ const AngularWorkflow = (defaultprops) => { } if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - console.log("IN NODE ADD") + if (addedLabels.includes(curaction.category_label[0])) { + continue + } + + if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { + continue + } cy.add({ group: "nodes", @@ -6240,12 +6816,13 @@ const AngularWorkflow = (defaultprops) => { }, position: { x: px, - y: py + (added * 50), + y: py + (addedLabels.length * 50), }, + locked: true, }) - added += 1 - if (added >= 3) { + addedLabels.push(curaction.category_label[0]) + if (addedLabels.length >= 2) { break } } @@ -6540,8 +7117,6 @@ const AngularWorkflow = (defaultprops) => { const allNodes = cy.nodes().jsons(); if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - console.log("In this :)") - var found = false; for (let nodekey in allNodes) { const currentNode = allNodes[nodekey]; @@ -6555,7 +7130,6 @@ const AngularWorkflow = (defaultprops) => { } 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())) @@ -7087,6 +7661,11 @@ const AngularWorkflow = (defaultprops) => { } */ + 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, @@ -7096,6 +7675,7 @@ const AngularWorkflow = (defaultprops) => { 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. @@ -7125,7 +7705,6 @@ const AngularWorkflow = (defaultprops) => { return edge; }); - console.log("VISUAL BRANCHES: ", inputworkflow.visual_branches) if (inputworkflow.visual_branches !== undefined && inputworkflow.visual_branches !== null && inputworkflow.visual_branches.length > 0) { const visualedges = inputworkflow.visual_branches.map((branch, index) => { const edge = {}; @@ -7187,8 +7766,6 @@ const AngularWorkflow = (defaultprops) => { } else { setElements(insertedNodes); } - - console.log("Setupgraph done 2!") } const removeNode = (nodeId) => { @@ -7350,7 +7927,6 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (responseJson.success !== false) { - console.log("Usecases: ", usecases) setUsecases(responseJson) } else { } @@ -7391,6 +7967,7 @@ const AngularWorkflow = (defaultprops) => { } setAllRevisions(responseJson) + setSelectedVersion(responseJson[0]) }) .catch((error) => { console.log("Error getting workflow revisions: ", error) @@ -7402,6 +7979,7 @@ const AngularWorkflow = (defaultprops) => { if (firstrequest) { setFirstrequest(false); getWorkflow(props.match.params.key, {}); + getChildWorkflows(props.match.params.key) getRevisionHistory(props.match.params.key) getApps() fetchUsecases() @@ -7502,7 +8080,7 @@ const AngularWorkflow = (defaultprops) => { loopAllowed: function (node) { return false; }, - }); + }) //cy.edgehandles({ // preview: false, @@ -7606,7 +8184,7 @@ const AngularWorkflow = (defaultprops) => { trigger.status = "stopped"; setSelectedTrigger(trigger); setWorkflow(workflow); - saveWorkflow(workflow); + saveWorkflow(workflow) }) .catch((error) => { console.log("Stop schedule error: ", error.toString()) @@ -7690,8 +8268,8 @@ const AngularWorkflow = (defaultprops) => { return; } - var mappedStartnode = "" - const alledges = cy.edges().jsons() + var mappedStartnode = "" + const alledges = cy.edges().jsons() if (alledges !== undefined && alledges !== null && alledges.length > 0) { for (let edgekey in alledges) { const tmp = alledges[edgekey] @@ -7784,10 +8362,10 @@ const AngularWorkflow = (defaultprops) => { backgroundColor: theme.palette.surfaceColor, cursor: "pointer", display: "flex", - }; + } - const VariableItem = (props) => { - const { variable, index, type } = props; + const VariableItem = (props) => { + const { variable, index, type } = props; const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -8039,7 +8617,7 @@ const AngularWorkflow = (defaultprops) => { }) }} > - New execution variable + New Runtime variable
    @@ -8047,11 +8625,15 @@ const AngularWorkflow = (defaultprops) => { ); }; - const handleSetTab = (event, newValue) => { - setCurrentView(newValue); - }; + const HandleLeftView = () => { + // console.log("HandleLeftView Rendered!") + + const handleSetTab = (event, newValue) => { + setCurrentView(newValue); + }; + // Defaults to apps. var thisview = ( { 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 @@ -8558,7 +9146,25 @@ const AngularWorkflow = (defaultprops) => { ? "cloud" : environments[defaultEnvironmentIndex] === undefined ? "cloud" - : environments[defaultEnvironmentIndex].Name; + : 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) { @@ -8606,7 +9212,7 @@ const AngularWorkflow = (defaultprops) => { app.categories.length > 0 ? app.categories[0] : "", - authentication_id: "", + authentication_id: authId, finished: false, template: app.template === true ? true : false, }; @@ -8641,7 +9247,7 @@ const AngularWorkflow = (defaultprops) => { const AppView = (props) => { const { allApps, prioritizedApps, filteredApps, extraApps } = props; - + // console.log("AppView Rendered!") //extraApps, const [visibleApps, setVisibleApps] = React.useState( Array.prototype.concat.apply( @@ -9295,7 +9901,9 @@ const AngularWorkflow = (defaultprops) => {
    ); - }; +} + + const getNextActionName = (appName) => { var highest = ""; @@ -9488,30 +10096,48 @@ const AngularWorkflow = (defaultprops) => { } } - setSelectedAction(newSelectedAction); - setUpdate(Math.random()); + // Last fix for params + if (newSelectedAction.parameters !== undefined && newSelectedAction.parameters !== null && newSelectedAction.parameters.length > 0) { + for (let paramkey in newSelectedAction.parameters) { + const param = newSelectedAction.parameters[paramkey] + if (param.name !== "body") { + continue + } - const allNodes = cy.nodes().jsons(); - if (allNodes !== undefined && allNodes !== null) { - for (let nodekey in allNodes) { - const currentNode = allNodes[nodekey]; - if ( - currentNode.data.attachedTo === oldaction.id && - currentNode.data.isDescriptor - ) { - const foundnode = cy.getElementById(currentNode.data.id); - if (foundnode !== null && foundnode !== undefined) { - const iconInfo = GetIconInfo(newaction); - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - foundnode.data("image", svgpin_Url); - foundnode.data("imageColor", iconInfo.iconBackgroundColor); - } - - break; + if (param.example !== undefined && param.example !== null && param.example.length > 0) { + if (param.value === undefined || param.value === null || param.value.length === 0) { + param.value = param.example } } + + newSelectedAction.parameters[paramkey] = param } + } + + setSelectedAction(newSelectedAction) + setUpdate(Math.random()) + + const allNodes = cy.nodes().jsons() + if (allNodes !== undefined && allNodes !== null) { + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if ( + currentNode.data.attachedTo === oldaction.id && + currentNode.data.isDescriptor + ) { + const foundnode = cy.getElementById(currentNode.data.id); + if (foundnode !== null && foundnode !== undefined) { + const iconInfo = GetIconInfo(newaction); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + foundnode.data("image", svgpin_Url); + foundnode.data("imageColor", iconInfo.iconBackgroundColor); + } + + break; + } + } + } // Send it in here, after all fields are filled // Disabled for now :( @@ -9523,44 +10149,43 @@ const AngularWorkflow = (defaultprops) => { // appname & version // description // ACTION select - const selectedNameChange = (event) => { - event.target.value = event.target.value.replaceAll("(", ""); - event.target.value = event.target.value.replaceAll(")", ""); - event.target.value = event.target.value.replaceAll("]", ""); - event.target.value = event.target.value.replaceAll("[", ""); - event.target.value = event.target.value.replaceAll("{", ""); - event.target.value = event.target.value.replaceAll("}", ""); - event.target.value = event.target.value.replaceAll("*", ""); - event.target.value = event.target.value.replaceAll("!", ""); - event.target.value = event.target.value.replaceAll("@", ""); - event.target.value = event.target.value.replaceAll("#", ""); - event.target.value = event.target.value.replaceAll("$", ""); - event.target.value = event.target.value.replaceAll("%", ""); - event.target.value = event.target.value.replaceAll("&", ""); - event.target.value = event.target.value.replaceAll("#", ""); - event.target.value = event.target.value.replaceAll(".", ""); - event.target.value = event.target.value.replaceAll(",", ""); - event.target.value = event.target.value.replaceAll(" ", "_"); - event.target.value = event.target.value.replaceAll("^", "_"); - event.target.value = event.target.value.replaceAll("'", "_"); - event.target.value = event.target.value.replaceAll("\"", "_"); - event.target.value = event.target.value.replaceAll("\"", "_"); - event.target.value = event.target.value.replaceAll(":", "_"); - event.target.value = event.target.value.replaceAll(";", "_"); - event.target.value = event.target.value.replaceAll("=", "_"); - event.target.value = event.target.value.replaceAll("+", "_"); - - selectedAction.label = event.target.value; + const selectedNameChange = (appActionName) => { + appActionName = appActionName.replaceAll("(", ""); + appActionName = appActionName.replaceAll(")", ""); + appActionName = appActionName.replaceAll("]", ""); + appActionName = appActionName.replaceAll("[", ""); + appActionName = appActionName.replaceAll("{", ""); + appActionName = appActionName.replaceAll("}", ""); + appActionName = appActionName.replaceAll("*", ""); + appActionName = appActionName.replaceAll("!", ""); + appActionName = appActionName.replaceAll("@", ""); + appActionName = appActionName.replaceAll("#", ""); + appActionName = appActionName.replaceAll("$", ""); + appActionName = appActionName.replaceAll("%", ""); + appActionName = appActionName.replaceAll("&", ""); + appActionName = appActionName.replaceAll("#", ""); + appActionName = appActionName.replaceAll(".", ""); + appActionName = appActionName.replaceAll(",", ""); + appActionName = appActionName.replaceAll(" ", "_"); + appActionName = appActionName.replaceAll("^", "_"); + appActionName = appActionName.replaceAll("'", "_"); + appActionName = appActionName.replaceAll("\"", "_"); + appActionName = appActionName.replaceAll("\"", "_"); + appActionName = appActionName.replaceAll(":", "_"); + appActionName = appActionName.replaceAll(";", "_"); + appActionName = appActionName.replaceAll("=", "_"); + appActionName = appActionName.replaceAll("+", "_"); + selectedAction.label = appActionName; setSelectedAction(selectedAction); }; - const actionDelayChange = (event) => { - if (isNaN(event.target.value)) { - console.log("NAN: ", event.target.value) + const actionDelayChange = (delay) => { + if (isNaN(delay)) { + console.log("NAN: ", delay) return } - const parsedNumber = parseInt(event.target.value) + const parsedNumber = parseInt(delay) if (parsedNumber > 86400) { console.log("Max number is 1 day (86400)") return @@ -10153,7 +10778,26 @@ const AngularWorkflow = (defaultprops) => { } } - const executionArgumentModal = + const handleAppAuthGroupCheckbox = (data) => { + console.log("CHECKED: ", data) + + if (workflow.auth_groups === undefined || workflow.auth_groups === null) { + workflow.auth_groups = [] + } + + const foundIndex = workflow.auth_groups.findIndex(auth => auth === data.id) + if (foundIndex >= 0) { + workflow.auth_groups.splice(foundIndex, 1) + } else { + workflow.auth_groups.push(data.id) + } + + setWorkflow(workflow) + console.log("WF: ", workflow) + setUpdate(Math.random()) + } + + const authgroupModal = { style={{ pointerEvents: "none" }} PaperComponent={PaperComponent} aria-labelledby="draggable-dialog-title" - open={executionArgumentModalOpen} + open={authgroupModalOpen} PaperProps={{ style: { padding: 30, @@ -10184,80 +10828,240 @@ const AngularWorkflow = (defaultprops) => { style={{ zIndex: 5000, position: "absolute", top: 34, right: 34 }} onClick={(e) => { e.preventDefault(); - setExecutionArgumentModalOpen(false); + setAuthgroupModalOpen(false) }} > - Provide a runtime argument + Authgroup Selection - At least one node in this workflow requires a runtime argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. + Authgroups are a way to control how a workflow runs. If chosen, the workflow will use the groups on the nodes that have them selected. If three are chosen, the workflows runs three times. This is an experimental MSSP feature to handle multiple environments. - {/* -
    - - { - setExecutionText(e.target.value); - }} - /> - -
    - */} - {availableArguments.length > 0 ? -
    - - Previously used arguments: - - {availableArguments.map((data) => { - return ( - { - setExecutionText(data) - executeWorkflow(data, workflow.start, lastSaved); - setExecutionArgumentModalOpen(false) - }} - > -
    - - {data} - - - ) - })} -
    - : null} + + {authGroups.map((data, index) => { + var checked = false + if (workflow.auth_groups !== undefined && workflow.auth_groups !== null) { + checked = workflow.auth_groups.includes(data.id) + } + + return ( +
    { + handleAppAuthGroupCheckbox(data) + }} + > + + + + {data.label} + + + + {data.environment} + + + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = appAuthentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })} + +
    + ) + })} +
    + const executionArgumentModal = + { + }} + > + + { + e.preventDefault(); + setExecutionArgumentModalOpen(false) + }} + > + + + + + Provide an execution argument + + + + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
    + {workflow.input_questions.map((question, index) => { + + return ( +
    + {question.name} + { + var newtext = {} + if (executionText.length > 0) { + try { + newtext = JSON.parse(executionText) + // Check if list or object, then make it object only + if (Array.isArray(newtext)) { + newtext = {} + } + } catch (e) { + console.log("Error parsing JSON: ", e) + } + } + + newtext[question.value] = e.target.value + setExecutionText(JSON.stringify(newtext)) + }} + /> +
    + ) + })} + + +
    + : +
    + + At least one node in this workflow requires an execution argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. + + + + {availableArguments.length > 0 ? +
    + + Previously used arguments: + + {availableArguments.map((data) => { + return ( + { + setExecutionText(data) + executeWorkflow(data, workflow.start, lastSaved); + + setExecutionArgumentModalOpen(false) + }} + > +
    + + {data} + + + ) + })} +
    + : null} + + +
    + } + +
    + const aiQueryModal = { {/* Check if dest is the same as start */} - {conditionsDisabled ? - - Conditions are unavailable between triggers and the startnode. - - : null} + {conditionsDisabled ? + + Conditions are unavailable between triggers and the startnode. + + : null}
    {/* @@ -11634,7 +12438,7 @@ const AngularWorkflow = (defaultprops) => { setSubworkflow(e.target.value); // Sets the startnode - if (e.target.value.id !== workflow.id) { + if (e.target.value.id !== workflow.id && e.target.value.id.length > 0 ) { console.log("WORKFLOW: ", e.target.value); const startnode = e.target.value.actions.find((action) => action.id === e.target.value.start); @@ -11718,7 +12522,7 @@ const AngularWorkflow = (defaultprops) => { return transformedData; - } + }; const AppAuthSelector = ({ appAuthData }) => { const [selectedAuth, setSelectedAuth] = useState(""); @@ -11731,8 +12535,11 @@ const AngularWorkflow = (defaultprops) => { const handleShowingValue = (appName) => { let mappingWithName = {} let listWithValues = workflow.triggers[selectedTriggerIndex].parameters[5]?.value.split(";").filter(e => e).map(e => e.split("=")) - console.log("LIST WITH VALUES: ", listWithValues) + if (listWithValues === undefined || listWithValues === null || listWithValues.length === 0) { + return "no-overrides"; + } + for (let i = 0; i < listWithValues.length; i++) { mappingWithName[listWithValues[i][0]] = listWithValues[i][1] } @@ -11803,9 +12610,7 @@ const AngularWorkflow = (defaultprops) => { // value: auth.id, // }); setSelectedAuth(auth.id); - }; - - console.log("TRANSFORMED AUTH DATA: ", transformedAuthData); + } return (
    @@ -11962,9 +12767,6 @@ const AngularWorkflow = (defaultprops) => { setActionlist(actionlist); } - // Shows nested list of nodes > their JSON lists - const ActionlistWrapper = (props) => { - const { data } = props; const handleMenuClose = () => { setUpdate(Math.random()); @@ -12010,302 +12812,7 @@ const AngularWorkflow = (defaultprops) => { marginRight: 15, }; - return ( - { - handleMenuClose(); - }} - open={!!menuPosition} - style={{ - border: `2px solid #f85a3e`, - color: "white", - marginTop: 2, - }} - > - {actionlist.map((innerdata) => { - const icon = - innerdata.type === "action" ? ( - - ) : innerdata.type === "workflow_variable" || - innerdata.type === "execution_variable" ? ( - - ) : ( - - ); - - const handleExecArgumentHover = (inside) => { - var exec_text_field = document.getElementById( - "execution_argument_input_field" - ); - if (exec_text_field !== null) { - if (inside) { - exec_text_field.style.border = "2px solid #f85a3e"; - } else { - exec_text_field.style.border = ""; - } - } - - // Also doing arguments - if ( - workflow.triggers !== undefined && - workflow.triggers !== null && - workflow.triggers.length > 0 - ) { - for (let triggerkey in workflow.triggers) { - const item = workflow.triggers[triggerkey]; - - if (cy !== undefined) { - var node = cy.getElementById(item.id); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } - } - } - } - } - } - - const handleActionHover = (inside, actionId) => { - if (cy !== undefined) { - var node = cy.getElementById(actionId); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } - } - } - }; - - const handleMouseover = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(true); - } else if (innerdata.type === "action") { - handleActionHover(true, innerdata.id); - } - }; - - const handleMouseOut = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(false); - } else if (innerdata.type === "action") { - handleActionHover(false, innerdata.id); - } - }; - - var parsedPaths = []; - console.log("Found example data: ", innerdata.example) - if (typeof innerdata.example === "object") { - parsedPaths = GetParsedPaths(innerdata.example, ""); - } - - const coverColor = "#82ccc3" - - return parsedPaths.length > 0 ? ( - - {/* - - {icon} {innerdata.name} -
    - } - parentMenuOpen={!!menuPosition} - style={{ - backgroundColor: theme.palette.inputColor, - color: "white", - minWidth: 250, - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - {parsedPaths.map((pathdata, index) => { - // FIXME: Should be recursive in here - const icon = - pathdata.type === "value" ? ( - - ) : pathdata.type === "list" ? ( - - ) : ( - - ) - - return ( - { }} - onClick={() => { - handleItemClick([innerdata, pathdata]); - }} - > - -
    - {icon} {pathdata.name} -
    -
    -
    - ); - })} - - */} - - - {icon} {innerdata.name} -
    - } - parentMenuOpen={!!menuPosition} - style={{ - color: "white", - minWidth: 250, - maxWidth: 250, - maxHeight: 50, - overflow: "hidden", - }} - onClick={() => { - console.log("CLICKED: ", innerdata); - console.log(innerdata.example) - handleItemClick([innerdata]); - }} - > - - - { - //console.log("HOVER: ", pathdata); - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - - {innerdata.name} - - - - {parsedPaths.map((pathdata, index) => { - // FIXME: Should be recursive in here - // - const icon = - pathdata.type === "value" ? ( - - ) : pathdata.type === "list" ? ( - - ) : ( - - ); - // - - const indentation_count = (pathdata.name.match(/\./g) || []).length+1 - const baseIndent =
    - //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 - const boxPadding = 0 - const namesplit = pathdata.name.split(".") - const newname = namesplit[namesplit.length-1] - return ( - { - //console.log("HOVER: ", pathdata); - }} - onClick={() => { - handleItemClick([innerdata, pathdata]); - }} - > - -
    - {Array(indentation_count).fill().map((subdata, subindex) => { - return ( - baseIndent - ) - })} - {icon} {newname} - {pathdata.type === "list" ? { - - }} /> : null} -
    -
    -
    - ); - })} - - - - ) : ( - handleMouseover()} - onMouseOut={() => { - handleMouseOut(); - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - -
    - {icon} {innerdata.name} -
    -
    -
    - ); - })} - - ); - }; - + if (Object.getOwnPropertyNames(selectedTrigger).length > 0) { if (workflow.triggers[selectedTriggerIndex] === undefined) { return null; @@ -12416,7 +12923,7 @@ const AngularWorkflow = (defaultprops) => { data: newbranch, }; - cy.add(cybranch) + cy.add(cybranch); } console.log("Value to be set: ", e.target.value); @@ -12643,9 +13150,11 @@ const AngularWorkflow = (defaultprops) => {
    )}
    + {workflows === undefined || workflows === null || workflows.length === 0 ? null : ( + { }} /> {!showDropdown ? null : - + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + border: `2px solid #f85a3e`, + color: "white", + marginTop: 2, + }} + > + {actionlist.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : ( + + ); + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #f85a3e"; + } else { + exec_text_field.style.border = ""; + } + } + + // Also doing arguments + if ( + workflow.triggers !== undefined && + workflow.triggers !== null && + workflow.triggers.length > 0 + ) { + for (let triggerkey in workflow.triggers) { + const item = workflow.triggers[triggerkey]; + + if (cy !== undefined) { + var node = cy.getElementById(item.id); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + } + } + } + + const handleActionHover = (inside, actionId) => { + if (cy !== undefined) { + var node = cy.getElementById(actionId); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + }; + + const handleMouseover = () => { + if (innerdata.type === "Execution Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Execution Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + console.log("Found example data: ", innerdata.example) + if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + + return parsedPaths.length > 0 ? ( + + {/* + + {icon} {innerdata.name} +
    + } + parentMenuOpen={!!menuPosition} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + minWidth: 250, + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ) + + return ( + { }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
    + {icon} {pathdata.name} +
    +
    +
    + ); + })} + + */} + + + {icon} {innerdata.name} +
    + } + parentMenuOpen={!!menuPosition} + style={{ + color: "white", + minWidth: 250, + maxWidth: 250, + maxHeight: 50, + overflow: "hidden", + }} + onClick={() => { + console.log("CLICKED: ", innerdata); + console.log(innerdata.example) + handleItemClick([innerdata]); + }} + > + + + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + + {innerdata.name} + + + + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + // + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ); + // + + const indentation_count = (pathdata.name.match(/\./g) || []).length+1 + const baseIndent =
    + //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 + const boxPadding = 0 + const namesplit = pathdata.name.split(".") + const newname = namesplit[namesplit.length-1] + return ( + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
    + {Array(indentation_count).fill().map((subdata, subindex) => { + return ( + baseIndent + ) + })} + {icon} {newname} + {pathdata.type === "list" ? { + + }} /> : null} +
    +
    +
    + ); + })} + + + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + +
    + {icon} {innerdata.name} +
    +
    +
    + ); + })} + } {/*
    {
    - {/*
    -
    -
    -
    - Auth Override -
    -
    +
    +
    +
    + Auth Override +
    +
    -
    -
    - -
    -
    -
    +
    +
    + +
    +
    +
    - */}
    - ) + ); } return null; @@ -13169,7 +13964,7 @@ const AngularWorkflow = (defaultprops) => { // Special SCHEDULE handler var trigger_header_auth = "" - if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined ) { + if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers !== null && workflow.triggers !== undefined && workflow.triggers.length >= selectedTriggerIndex && workflow.triggers[selectedTriggerIndex] !== undefined ) { if (selectedTrigger.trigger_type === "SCHEDULE" && workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null) { console.log("Autofixing schedule") @@ -13232,7 +14027,48 @@ const AngularWorkflow = (defaultprops) => { workflow.triggers[selectedTriggerIndex].parameters.length > 2 ? workflow.triggers[selectedTriggerIndex].parameters[2].value : ""; - } + }else if( + selectedTrigger.trigger_type === "USERINPUT" + ){ + if ( + workflow.triggers[selectedTriggerIndex].parameters === undefined || + workflow.triggers[selectedTriggerIndex].parameters === null || + workflow.triggers[selectedTriggerIndex].parameters.length === 0 + ) { + workflow.triggers[selectedTriggerIndex].parameters = []; + workflow.triggers[selectedTriggerIndex].parameters[0] = { + name: "alertinfo", + value: "Do you want to continue the workflow? Start parameters: $exec", + }; + + // boolean, + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "options", + value: "boolean", + }; + + // email,sms,app ... + workflow.triggers[selectedTriggerIndex].parameters[2] = { + name: "type", + value: "subflow", + }; + + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "email", + value: "test@test.com", + }; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "sms", + value: "0000000", + }; + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "subflow", + value: "", + }; + + setWorkflow(workflow); + } + } } const WebhookSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined || selectedTrigger.trigger_type !== "WEBHOOK" ? null : @@ -13356,6 +14192,7 @@ const AngularWorkflow = (defaultprops) => { setUpdate(Math.random()); } + document.activeElement.blur(); }} >
    @@ -14027,48 +14864,7 @@ const AngularWorkflow = (defaultprops) => { }) } - const UserinputSidebar = () => { - if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) { - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow.triggers[selectedTriggerIndex].parameters.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "alertinfo", - value: "Do you want to continue the workflow? Start parameters: $exec", - }; - - // boolean, - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "options", - value: "boolean", - }; - - // email,sms,app ... - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "type", - value: "subflow", - }; - - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "email", - value: "test@test.com", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "sms", - value: "0000000", - }; - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "subflow", - value: "", - }; - - setWorkflow(workflow); - } - - return ( + const UserinputSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined || selectedTrigger.trigger_type !== "USERINPUT" ? null :

    {selectedTrigger.app_name} @@ -14254,7 +15050,12 @@ const AngularWorkflow = (defaultprops) => { const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " "); return newname; }} - options={workflows} + options={ + [{ + "id": "", + "name": "No Workflow Selected", + }].concat(workflows) + } fullWidth style={{ backgroundColor: theme.palette.inputColor, @@ -14266,6 +15067,7 @@ const AngularWorkflow = (defaultprops) => { onChange={(event, newValue) => { console.log("Changed autocomplete!") handleWorkflowSelectionUpdate({ target: { value: newValue } }, true) + event.target.blur(); }} renderOption={(props, data, state) => { if (data.id === workflow.id) { @@ -14294,6 +15096,7 @@ const AngularWorkflow = (defaultprops) => { value: data, }}, true) + document.activeElement.blur(); }} > @@ -14341,6 +15144,7 @@ const AngularWorkflow = (defaultprops) => {

    ) : null} + {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ? ( @@ -14372,6 +15176,7 @@ const AngularWorkflow = (defaultprops) => { }} /> ) : null} + {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[ @@ -14404,13 +15209,67 @@ const AngularWorkflow = (defaultprops) => { ) : null}
    + +
    + Required Input-Questions + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
    + {workflow.input_questions.map((question, index) => { + var foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "input_questions") + + const selectionClick = () => { + if (foundParamIndex === -1) { + workflow.triggers[selectedTriggerIndex].parameters.push({ + "name": "input_questions", + "value": [], + }) + + foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1 + } else { + try { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.parse(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) + } catch (e) { + console.log("Couldn't parse input questions: ", e) + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = [] + } + } + + if (workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.includes(question.name)) { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.filter((item) => item !== question.name) + } else { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.push(question.name) + } + + // Make it back to a string + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.stringify(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) + setWorkflow(workflow) + setUpdate(Math.random()) + } + + return ( +
    { + selectionClick() + }}> + + + {question.name} + +
    + ) + })} +
    + : +
    { + setEditWorkflowModalOpen(true) + toast.info("Expand and scroll down to add input-questions") + }}> + No Input-Questions found. Click to add them! +
    + } +
    - ) - } - - return null - } - const PipelineSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined && selectedTrigger.trigger_type !== "SCHEDULE" ? null :

    @@ -14916,7 +15775,7 @@ const AngularWorkflow = (defaultprops) => {

    - const cytoscapeViewWidths = isMobile ? 50 : 850; + const cytoscapeViewWidths = isMobile ? 50 : 950; const bottomBarStyle = { position: "fixed", right: isMobile ? 20 : 20, @@ -14935,8 +15794,12 @@ const AngularWorkflow = (defaultprops) => { right: 0, left: isMobile ? 20 : leftBarSize + 20, top: isMobile ? 30 : appBarSize + 20, + pointerEvents: "none", } + + + const TopCytoscapeBar = (props) => { if (workflow.public === true) { return null @@ -14952,15 +15815,17 @@ const AngularWorkflow = (defaultprops) => {
    + {

    {workflow.name}

    - {isCorrectOrg ? null : + {!distributedFromParent ? + isCorrectOrg ? null : Warning: Change { toast("Changing to correct organisation. Please wait a few seconds.") @@ -15043,9 +15910,248 @@ const AngularWorkflow = (defaultprops) => { }} >Active Organization to edit this Workflow. + : + + Warning: This workflow is controlled by your parent org and may not be editable. + } + + {originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0 || originalWorkflow.suborg_distribution.includes("none") ? null : + + + + View Suborg workflow + + + + } + + + {authGroups !== undefined && authGroups !== null && authGroups.length > 0 ? + + + + + + : null} +
    -
    +
    {parentWorkflows.slice(0,5).map((wf, index) => { return ( @@ -15346,7 +16452,7 @@ const AngularWorkflow = (defaultprops) => { ) } - const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ? + const shownErrors = !distributedFromParent && !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
    { console.log("Show workflow revisions key pressed") if (!workflow.public) { setShowWorkflowRevisions(true) - setSelectedRevision(workflow) //setOriginalWorkflow(workflow) } } @@ -15477,8 +16582,6 @@ const AngularWorkflow = (defaultprops) => { } } - /* - // Infinitely annoying. Need a new bind if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { console.log("Shift key pressed") if (!workflow.public && executionModalOpen) { @@ -15490,8 +16593,7 @@ const AngularWorkflow = (defaultprops) => { setExecutionModalView(0); } } - */ - } + }; document.addEventListener('keydown', handleKeyDown); @@ -15522,7 +16624,6 @@ const AngularWorkflow = (defaultprops) => { console.log("Show workflow revisions key pressed") if (!workflow.public) { setShowWorkflowRevisions(true) - setSelectedRevision(workflow) //setOriginalWorkflow(workflow) } } @@ -15533,6 +16634,7 @@ const AngularWorkflow = (defaultprops) => { } } + /* if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { console.log("Shift key pressed") if (!workflow.public && executionModalOpen) { @@ -15544,6 +16646,7 @@ const AngularWorkflow = (defaultprops) => { setExecutionModalView(0); } } + */ }; document.addEventListener('keydown', handleKeyDown); @@ -15594,6 +16697,106 @@ const AngularWorkflow = (defaultprops) => { ) } + // Used for handling suborg workflow distribution management + const updateCurrentWorkflow = (inputworkflow) => { + setLastSaved(false) + setSelectedAction({}); + setSelectedApp({}) + setWorkflow(inputworkflow) + + // Update props match key + if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { + setDistributedFromParent(inputworkflow.parentorg_workflow) + } else { + setDistributedFromParent("") + } + + 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"); + + setElements([]) + + // Remove all edges + cy.edges().remove() + cy.nodes().remove() + } + + // Remove all cytoscape triggers first? + /* + setTimeout(() => { + setupGraph(inputworkflow) + + 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)); + }, 25) + */ + + } + + // Uses Org-Id referencing header to create a workflow while getting it in realtime + // This further ensures the user needs access to GET the workflow properly + const duplicateParentWorkflow = (inputWorkflow, org_id, setWorkflow) => { + fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { + method: "GET", + headers: { + "Org-Id": org_id, + "Content-Type": "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getChildWorkflows(inputWorkflow.id) + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + //toast("Failed to duplicate workflow") + } else { + //toast("Successfully duplicated workflow. Reloading child workflows.") + if (setWorkflow === true) { + updateCurrentWorkflow(responseJson) + } + } + }) + .catch((error) => { + console.log("Dupe workflow for suborg error: ", error.toString()) + }) + } + const BottomCytoscapeBar = () => { if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) { return null; @@ -15616,11 +16819,11 @@ const AngularWorkflow = (defaultprops) => { ) : ( - - - ); + ) return (
    @@ -15669,6 +16871,7 @@ const AngularWorkflow = (defaultprops) => { /> )} + {/*userdata.avatar === creatorProfile.github_avatar ? null :*/} @@ -15895,7 +17098,6 @@ const AngularWorkflow = (defaultprops) => { variant={"outlined"} onClick={() => { setShowWorkflowRevisions(true) - setSelectedRevision(workflow) //setOriginalWorkflow(workflow) }} > @@ -15903,6 +17105,8 @@ const AngularWorkflow = (defaultprops) => { + +
    ); @@ -15934,122 +17138,23 @@ const AngularWorkflow = (defaultprops) => { }; const RightSideBar = (props) => { - const { - //workflow, - //setWorkflow, - //setSelectedAction, - //setUpdate, - //selectedApp, - //workflowExecutions, - //setSelectedResult, - //selectedAction, - //setSelectedApp, - //setSelectedTrigger, - //setSelectedEdge, - //setCurrentView, - //cy, - //setAuthenticationModalOpen, - //setVariablesModalOpen, - //setCodeModalOpen, - //selectedNameChange, - //rightsidebarStyle, - //showEnvironment, - //selectedActionEnvironment, - //environments, - //setNewSelectedAction, - //appApiViewStyle, - //globalUrl, - //setSelectedActionEnvironment, - //requiresAuthentication, - //scrollConfig, - //setScrollConfig, - } = props; - - if (!rightSideBarOpen) { - return null; - } var defaultReturn = null - if (Object.getOwnPropertyNames(selectedAction).length > 0) { - if (Object.getOwnPropertyNames(selectedAction).length === 0) { - return null; - } - - defaultReturn = - - } else if (Object.getOwnPropertyNames(selectedComment).length > 0) { + if (Object.getOwnPropertyNames(selectedComment).length > 0) { defaultReturn = } else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) { - if (selectedTrigger.trigger_type === "SCHEDULE") { - // Handled elsewhere as an experiment - defaultReturn = null - } else if (selectedTrigger.trigger_type === "WEBHOOK") { - defaultReturn = null - } else if (selectedTrigger.trigger_type === "SUBFLOW") { - defaultReturn = - } else if (selectedTrigger.trigger_type === "EMAIL") { + if (selectedTrigger.trigger_type === "EMAIL") { defaultReturn = - } else if (selectedTrigger.trigger_type === "USERINPUT") { - defaultReturn = } else if (selectedTrigger.trigger_type === undefined) { //defaultReturn = return null; } else { + /* console.log( "Unable to handle invalid trigger type " + selectedTrigger.trigger_type ); + */ return null; } } else if (Object.getOwnPropertyNames(selectedEdge).length > 0) { @@ -16600,7 +17705,8 @@ const AngularWorkflow = (defaultprops) => { // This is the playbutton at 150x150 const defaultImage = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg=="; + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" + const size = 40; const borderRadius = 5 if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { @@ -16617,7 +17723,27 @@ const AngularWorkflow = (defaultprops) => { ) } - if (execution.execution_source === "webhook") { + if (execution.execution_source === "authgroups") { + const iconMargin = 7 + return ( +
    + +
    + ) + + } else if (execution.execution_source === "webhook") { return ( {"webhook"} { console.log("IN useeffectt (2)" + collapsed) return; } - }) + },[]) /* componentWillUpdate = (nextProps, nextState) => { console.log(nextProps, nextState) @@ -16953,6 +18079,71 @@ const AngularWorkflow = (defaultprops) => { ) } + const changeExecution = (data) => { + if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { + start() + setExecutionRunning(true) + setExecutionRequestStarted(false) + } + + var checkStarted = false + if (data.results !== undefined && data.results !== null && data.results.length > 0) { + if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + if (data.results !== undefined && data.results !== null) { + for (let resultkey in data.results) { + if (data.results[resultkey].status !== "SUCCESS") { + continue + } + + if (data.results[resultkey].result.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + break + } + } + } + } + } + + const cur_execution = { + execution_id: data.execution_id, + authorization: data.authorization, + } + + setExecutionRequest(cur_execution) + setExecutionModalView(1) + + if (!checkStarted) { + handleUpdateResults(data, cur_execution) + + if (cy !== undefined && cy !== null) { + cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); + for (let actionKey in data.workflow.actions) { + var actionitem = data.workflow.actions[actionKey] + + handleColoring(actionitem.id, "", actionitem.label) + } + + for (let resultKey in data.results) { + var item = data.results[resultKey] + + handleColoring(item.action.id, item.status, item.action.label) + } + } + + setExecutionData(data) + } + } + const ShowCopyingTooltip = () => { const [showCopying, setShowCopying] = React.useState(true) @@ -17223,8 +18414,8 @@ const AngularWorkflow = (defaultprops) => { /> 0 ? ` Authgroup: ${data.authgroup}` : '')} + placement="left" >
    { {data.workflow.actions !== null ? (
    { marginBottom: "auto", }} > - {successActions} / {skippedActions > 0 ? skippedActions : {skippedActions}} / {calculatedResult} + {successActions} + {skippedActions > 0 ? skippedActions : {skippedActions}} = {calculatedResult}
    ) : null}
    {foundnotifications > 0 ? - + { @@ -17381,7 +18572,7 @@ const AngularWorkflow = (defaultprops) => {

    Details

    @@ -17427,7 +18618,80 @@ const AngularWorkflow = (defaultprops) => { ) : null} - {isCloud ? ( + + + + + + + + + + + + + + {isCloud ? { - ) : null} + : null} +
    - {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 && executionData.workflow.actions[0].environment !== "Cloud" ? + {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 ?
    Env      @@ -17481,16 +18746,29 @@ const AngularWorkflow = (defaultprops) => { {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && - executionData.execution_source !== "default" ? ( + executionData.execution_source !== "default" || + (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? (
    Source    - {executionData.execution_parent !== null && + + + {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + + Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' + + : + executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? ( - executionData.execution_source === props.match.params.key ? ( + executionData.execution_source === props.match.params.key ? { @@ -17502,7 +18780,7 @@ const AngularWorkflow = (defaultprops) => { > Parent Execution - ) : ( + : { Parent Workflow ) - ) : ( + : executionData.execution_source === "questions" || executionData.execution_source === "web" ? { : executionData.execution_source - )} + }
    ) : null} @@ -18068,21 +19346,49 @@ const AngularWorkflow = (defaultprops) => { validate.result = JSON.parse(validate.result) } - const AppResultVariable = ({ data }) => { + const AppResultVariable = ({ data, action }) => { const [open, setOpen] = React.useState(false) const showVariable = data.value.length < 60 // Check if it's valid JSON const checked = validateJson(data.value.trim()) + if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { + return ( +
    + + Action Logs + + + Logs for an action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. + +
    + ) + } + + var showlink = false + if (data.name.endsWith("-Url")) { + //data.name = data.name.toLowerCase().replaceAll("-", "_") + if (data.value.startsWith(", ")) { + data.value = data.value.substring(2) + } + + if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) { + showlink = true + } + } + return (
    {data.value.length > 60 || checked.valid ? { variant="body2" style={{ whiteSpace: 'pre-line', + color: showlink ? "#f85a3e" : "white", + cursor: showlink ? "pointer" : "default", }} - color="textSecondary" + onClick={(e) => { + if (showlink) { + e.preventDefault() + e.stopPropagation() + window.open(data.value, "_blank") + } + }} + color={showlink ? "inherit" : "textSecondary"} > {data.value} @@ -18204,11 +19519,10 @@ const AngularWorkflow = (defaultprops) => { return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information." } - /* if (result.status === 200 || result.status === 201 || result.status === 204) { return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct." } - */ + // Validate and check for newlines if (result.success !== false) { @@ -18248,6 +19562,10 @@ const AngularWorkflow = (defaultprops) => { return "Consider whether your Orborus environment can connect to a local IP or not." } + if (stringjson.includes("kms/")) { + return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=priorities. If you need help with KMS, please contact support@shuffler.io" + } + if (stringjson.includes("invalidurl")) { // IF count of "http" is more than one, 1, it's prolly invalid var additionalinfo = "" @@ -18273,7 +19591,7 @@ const AngularWorkflow = (defaultprops) => { if (stringjson.includes("connectionerror")) { if (stringjson.includes("kms")) { - return "KMS authentication failed. Check your notifications for more details." + return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=priorities&kms=true. If you need help with KMS, please contact support@shuffler.io" } return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." @@ -18353,7 +19671,7 @@ const AngularWorkflow = (defaultprops) => { const result = execution.results.find((data) => data.status === "SUCCESS" && data.action.id === selectedResult.action.id) if (result !== undefined) { - const oldstartnode = cy.getElementById(selectedResult.action.id); + const oldstartnode = cy.getElementById(selectedResult.action.id) if (oldstartnode !== undefined && oldstartnode !== null) { const foundname = oldstartnode.data("label") if (foundname !== undefined && foundname !== null) { @@ -18361,8 +19679,8 @@ const AngularWorkflow = (defaultprops) => { } } - setSelectedResult(result); - setUpdate(Math.random()); + setSelectedResult(result) + setUpdate(Math.random()) break; } } @@ -18619,7 +19937,7 @@ const AngularWorkflow = (defaultprops) => { } return ( - + ); })}
    @@ -18688,12 +20006,68 @@ const AngularWorkflow = (defaultprops) => {
    {executionModal} - - + + { + rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ? +
    + +
    : null + } {/* Looks for triggers" */} {/* Only fixed the ones that require scrolling on a small screen */} {/* Most important: Actions. But these are a lot more complex */} - {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE") ? + {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT") ?
    {Object.getOwnPropertyNames(selectedTrigger).length > 0 ? selectedTrigger.trigger_type === "SCHEDULE" ? @@ -18702,10 +20076,19 @@ const AngularWorkflow = (defaultprops) => { PipelineSidebar : selectedTrigger.trigger_type === "WEBHOOK" ? WebhookSidebar + : selectedTrigger.trigger_type === "USERINPUT" ? + UserinputSidebar : null : null}
    : null} + + { + rightSideBarOpen && selectedTrigger.trigger_type === "SUBFLOW"&& Object.getOwnPropertyNames(selectedTrigger).length > 0 ? +
    + +
    : null + } {/* { //if (configureWorkflowModalOpen) { // setSelectedAction({}); //} + // + setSelectedMeta(undefined) }} PaperProps={{ style: { @@ -19582,11 +20967,27 @@ const AngularWorkflow = (defaultprops) => { overflowY: "auto", overflowX: "hidden", }} + onLoad={() => { + /* + if (isCloud && ReactGA !== undefined) { + toast("Sending GA info") + // Google analytics info about what app people are looking at + ReactGA.event({ + category: "workflow", + action: `documentation_load`, + label: selectedApp.name, + }) + + } + */ + }} > {selectedApp.documentation === undefined || selectedApp.documentation === null || selectedApp.documentation.length === 0 ? ( - + { style={{ marginTop: 25, marginBottom: 25, - backgroundColor: theme.palette.inputColor, + backgroundColor: "rgba(255,255,255,0.6)", }} /> - - There is currently no extended documentation available for this - app. - + +
    + + There is no Shuffle-specific documentation for this app yet. Documentation is written custom for each app, and is a community effort. Hope to see your contribution + + +
    + - Want to help the making of, or imrpvoe this app?{" "} + Want to help the making of, or improve this app?{" "} +
    { )}
    ) : ( +
    + {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
    +
    + {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    +
    + {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + : null} { > {selectedApp.documentation} +
    )}
    @@ -19714,9 +21218,8 @@ const AngularWorkflow = (defaultprops) => {
    Configuration options for {selectedOption}
    - - {selectedOption === "Kafka Queue" ? -
    + {selectedOption === "Kafka Queue" && ( + <> Topic { placeholder={"earliest"} defaultValue={(selectedTrigger?.parameters?.find(param => param.name === "auto_offset_reset")?.value) || ''} /> */} -
    - : null} - - param.name === "bootstrap_servers")?.value) || ''} - /> - + + )}
    - {selectedRevision.name} + {selectedVersion.name}
    {/* Cross icon to close it */} @@ -20501,6 +21735,8 @@ const AngularWorkflow = (defaultprops) => { const changeActionParameterCodeMirror = (event, count, data, actionlist) => { // Check if event.target.value is an array. If it is, split with comma + console.log("1 - SELECTED ACTION: ", selectedAction) + console.log("1 - DATA: ", data) if (data.startsWith("${") && data.endsWith("}")) { // PARAM FIX - Gonna use the ID field, even though it's a hack @@ -20508,15 +21744,12 @@ const AngularWorkflow = (defaultprops) => { if (paramcheck !== undefined) { // Escapes all double quotes const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); - console.log("REPLACE WITH: ", toReplace) if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { paramcheck["value_replace"] = [{ "key": data.name, "value": toReplace, }] - console.log("IN IF: ", paramcheck) - } else { const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) if (subparamindex === -1) { @@ -20527,8 +21760,6 @@ const AngularWorkflow = (defaultprops) => { } else { paramcheck["value_replace"][subparamindex]["value"] = toReplace } - - console.log("IN ELSE: ", paramcheck) } if (paramcheck["value_replace"] === undefined) { @@ -20544,8 +21775,6 @@ const AngularWorkflow = (defaultprops) => { } if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) { - console.log("GET THE LAST ARGUMENT FOR NODE!") - var curstring = "" var record = false for (let [key,keyval] in Object.entries(selectedAction.parameters[count].value)) { @@ -20560,7 +21789,6 @@ const AngularWorkflow = (defaultprops) => { } } - //console.log("CURSTRING: ", curstring) if (curstring.length > 0 && actionlist !== null) { // Search back in the action list curstring = curstring.split(" ").join("_").toLowerCase() @@ -20581,8 +21809,32 @@ const AngularWorkflow = (defaultprops) => { } } - selectedAction.parameters[count].autocompleted = false - selectedAction.parameters[count].value = data + console.log("2 - SELECTED ACTION: ", selectedAction) + console.log("2 - DATA: ", data) + + if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { + const parsedvalue = data + console.log("Parsed value: ", parsedvalue) + if (parsedvalue.includes("#")) { + const splitparsed = parsedvalue.split(".#.") + //console.log("Cant contain #: ", splitparsed) + if (splitparsed.length > 1) { + console.log("IN HERE AY") + //data.value = splitparsed[0] + + selectedAction.parameters[0].value = splitparsed[0] + selectedAction.parameters[1].value = splitparsed[1] + + selectedAction.parameters[0].autocompleted = true + selectedAction.parameters[1].autocompleted = true + setUpdate(Math.random()) + } + } + } else { + selectedAction.parameters[count].autocompleted = false + selectedAction.parameters[count].value = data + } + setSelectedAction(selectedAction) //setUpdate(Math.random()) } @@ -20657,6 +21909,7 @@ const AngularWorkflow = (defaultprops) => { {authenticationModal} {tenzirConfigModal} {/*editWorkflowModal*/} + {authgroupModal} {executionArgumentModal} {configureWorkflowModal} {/*usecaseSlidein*/} @@ -20768,7 +22021,7 @@ const AngularWorkflow = (defaultprops) => {
    ) : (
    - + Loading Workflow & Apps... diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 1000f7a0..4c816623 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -955,7 +955,6 @@ const Apps = (props) => { : null - console.log("Sharing config: ", sharingConfiguration); const activateButton = selectedApp.generated && !selectedApp.activated ? (
    @@ -1281,10 +1280,15 @@ const Apps = (props) => { {isCloud && !internalIds.includes(selectedApp.name.toLowerCase()) ?
    } - {buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? + {/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? finalize workflow animation { console.log("Img loaded.") @@ -868,7 +1005,7 @@ const RunWorkflow = (defaultprops) => { }} /> - : null} + : null*/}
    {executionInfo} diff --git a/frontend/src/views/Search.jsx b/frontend/src/views/Search.jsx index 8994dbed..b17080cf 100644 --- a/frontend/src/views/Search.jsx +++ b/frontend/src/views/Search.jsx @@ -43,11 +43,10 @@ const Search = (props) => { const params = Object.fromEntries(urlSearchParams.entries()); const foundTab = params["tab"]; if (foundTab !== null && foundTab !== undefined) { - for (var key in Object.keys(views)) { + for (let key in views) { const value = views[key]; - console.log(key, value); if (value === foundTab) { - setConfig("", key); + setConfig(null, key); break; } } @@ -130,7 +129,7 @@ const Search = (props) => { display: "flex", flexDirection: "column", overflowX: "hidden", - minHeight: 400, + minHeight: 400 }; const views = { @@ -138,7 +137,9 @@ const Search = (props) => { 1: "workflows", 2: "docs", 3: "creators", + 4: "discord" }; + const setConfig = (event, inputValue) => { const newValue = parseInt(inputValue); @@ -216,7 +217,7 @@ const Search = (props) => {
    { textColor="secondary" onChange={setConfig} aria-label="disabled tabs example" - variant="scrollable" - scrollButtons="auto" + // variant="scrollable" + // scrollButtons="auto" classes={{ indicator: classes.hideIndicator, root: classes.customTab }} > { const { globalUrl, isLoaded, userdata, setUserData } = props; @@ -32,6 +36,11 @@ const Settings = (props) => { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [newPassword2, setNewPassword2] = useState(""); + const [selectedOrganization, setSelectedOrganization] = useState({}); + const [MFARequired, setMFARequired] = React.useState(false); + const [image2FA, setImage2FA] = React.useState(""); + const [value2FA, setValue2FA] = React.useState(""); + // const [file, setFile] = React.useState(""); // const [fileBase64, setFileBase64] = React.useState( // userdata.image === undefined || userdata.image === null @@ -87,6 +96,82 @@ const Settings = (props) => { flexDirection: "column", }; + useEffect(() => { + if (userdata.active_org === undefined || userdata.active_org === null || userdata.active_org.id === undefined || userdata.active_org.id === null) { + return; + } + + fetch(`${globalUrl}/api/v1/orgs/${userdata.active_org.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + console.log("Received response:", response); + if (response.status !== 200) { + console.error("Status not 200:", response.status); + throw new Error(`Status not 200: ${response.status}`); + } + return response.json(); + }) + .then((responseJson) => { + setSelectedOrganization(responseJson); + setMFARequired(responseJson.mfa_required); + }) + .catch((error) => { + console.error("Error fetching organization:", error); + }); + }, [userdata]); + + const UpdateMFAInUserOrg = (org_id) => { + + if (MFARequired === false) { + toast("Making MFA required for your organization. Please wait..."); + } else { + toast("Making MFA optional for your organization. Please wait..."); + } + + const data = { + mfa_required: !selectedOrganization.mfa_required, + org_id: selectedOrganization.id, + } + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + console.log(responseJson) + if (responseJson["success"] === false) { + toast.error("Failed updating org: ", responseJson.reason); + } else { + if (MFARequired === false) { + setMFARequired(true) + toast.success("Successfully make MFA required for your organization!"); + } else { + setMFARequired(false) + toast.success("Successfully make MFA optional for your organization!") + } + } + }), + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + } + const checkOwner = (data, userdata) => { var currentOwner = false; if (data.owner.address === userdata.eth_info.account) { @@ -938,8 +1023,7 @@ const Settings = (props) => { > Submit password change -

    {passwordFormMessage}

    - +

    {passwordFormMessage}

    {isCloud && ( <> diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 5c47c76b..816f17c5 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -74,6 +74,7 @@ import { ArrowLeft as ArrowLeftIcon, ArrowRight as ArrowRightIcon, QueryStats as QueryStatsIcon, + Visibility as VisibilityIcon, } from "@mui/icons-material"; import { DataGrid, GridToolbar } from "@mui/x-data-grid"; @@ -129,7 +130,6 @@ export const GetIconInfo = (action) => { // Finds the icon based on the action. Should be verbs. const iconList = [ { key: "cases", values: ["cases"] }, - { key: "communication", values: ["communication", "comms", "email",] }, { key: "cache_add", values: ["set_cache"] }, { key: "cache_get", values: ["get_cache"] }, { key: "filter", values: ["filter"] }, @@ -199,6 +199,7 @@ export const GetIconInfo = (action) => { values: ["compare", "convert", "to", "filter", "translate", "parse"], }, { key: "close", values: ["close", "stop", "cancel", "block"] }, + { key: "communication", values: ["communication", "comms", "email", "mail",] }, ]; var selectedKey = "" @@ -614,6 +615,7 @@ const Workflows = (props) => { const [videoViewOpen, setVideoViewOpen] = React.useState(false) const [gettingStartedItems, setGettingStartedItems] = React.useState([]) const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([]) + const [highlightIds, setHighlightIds] = React.useState([]) const [apps, setApps] = React.useState([]); @@ -974,15 +976,16 @@ const Workflows = (props) => { }, 1000); } else if (selectedWorkflowIndexes.length > 0) { // Do backwards so it doesn't change + toast("Starting deletion of workflows. This might take a while.") for (var i = selectedWorkflowIndexes.length - 1; i >= 0; i--) { const workflow = filteredWorkflows[selectedWorkflowIndexes[i]-1] if (workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null) { - deleteWorkflow(workflow.id); + deleteWorkflow(workflow.id, true) } } setTimeout(() => { - getAvailableWorkflows(); + getAvailableWorkflows() }, 1000); setSelectedWorkflowIndexes([]); @@ -1113,7 +1116,7 @@ const Workflows = (props) => { }) } - const getAvailableWorkflows = () => { + const getAvailableWorkflows = (amount) => { var storageWorkflows = [] try { const storagewf = localStorage.getItem("workflows") @@ -1130,7 +1133,12 @@ const Workflows = (props) => { //console.log("Failed to get workflows from localstorage: ", e) } - fetch(globalUrl + "/api/v1/workflows", { + var url = `${globalUrl}/api/v1/workflows` + if (amount !== undefined && amount !== null) { + url += `?top=${amount}` + } + + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -1154,10 +1162,11 @@ const Workflows = (props) => { }) .then((responseJson) => { if (responseJson !== undefined) { + var newarray = [] - for (var key in responseJson) { - const wf = responseJson[key] - if (wf.public === true) { + for (var wfkey in responseJson) { + const wf = responseJson[wfkey] + if (wf.public === true || wf.hidden === true) { continue } @@ -1170,9 +1179,9 @@ const Workflows = (props) => { var parsedactionlist = []; for (var key in newarray) { const workflow = newarray[key] - if (workflow.status === "production") { - setProdFilter = true - } + //if (workflow.status === "production") { + // setProdFilter = true + //} for (var actionkey in newarray[key].actions) { const action = newarray[key].actions[actionkey]; @@ -1213,7 +1222,24 @@ const Workflows = (props) => { } setFirstLoad(false) - }, 100) + }, 250) + + /* + setTimeout(() => { + var timeout = 0 + for (var key in newarray) { + const wf = newarray[key] + if (wf.actions === undefined || wf.actions === null || wf.actions.length === 0) { + setTimeout(() => { + sideloadWorkflow(wf.id, false) + }, timeout) + + timeout += 1000 + } + + } + }, 1000) + */ } else { if (isLoggedIn) { @@ -1319,8 +1345,21 @@ const Workflows = (props) => { setView(tmpView); } + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["top"]; + if (foundTab !== null && foundTab !== undefined) { + // Check if it's a number + if (isNaN(foundTab)) { + getAvailableWorkflows() + } else { + getAvailableWorkflows(foundTab) + } + } else { + getAvailableWorkflows() + } + getApps() - getAvailableWorkflows(); getFramework() } }, []) @@ -1637,20 +1676,24 @@ const Workflows = (props) => { }); }; - const copyWorkflow = (data) => { - data = JSON.parse(JSON.stringify(data)); - toast("Copying workflow " + data.name); - data.id = ""; - data.name = data.name + "_copy"; - data = deduplicateIds(data, true); + const duplicateWorkflow = (data) => { + //data = JSON.parse(JSON.stringify(data)); + toast("Copying workflow '" + data.name + "'. The new workflow will load in and be highlighted."); + //data.id = ""; + //data.name = data.name + "_copy"; + //data = deduplicateIds(data, true); - fetch(globalUrl + "/api/v1/workflows", { + const duplicateData = { + name: data.name + "_copy", + } + + fetch(`${globalUrl}/api/v1/workflows/${data.id}/duplicate`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(data), + body: JSON.stringify(duplicateData), credentials: "include", }) .then((response) => { @@ -1660,7 +1703,20 @@ const Workflows = (props) => { } return response.json(); }) - .then(() => { + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + toast("Failed copying workflow: " + responseJson.reason) + } else { + toast("Failed copying workflow") + } + + return + } + + if (responseJson.id !== undefined) { + setHighlightIds([responseJson.id]) + } setTimeout(() => { getAvailableWorkflows(); }, 1000); @@ -1689,15 +1745,68 @@ const Workflows = (props) => { }) } - const sideloadWorkflow = (id, openEdit) => { + const exportSingleWorkflow = (data, setOpen) => { + setExportModalOpen(true) + + if (data.triggers !== null && data.triggers !== undefined) { + var newSubflows = []; + for (var key in data.triggers) { + const trigger = data.triggers[key]; + + if ( + trigger.parameters !== null && + trigger.parameters !== undefined + ) { + for (var subkey in trigger.parameters) { + const param = trigger.parameters[subkey]; + if ( + param.name === "workflow" && + param.value !== data.id && + !newSubflows.includes(param.value) + ) { + newSubflows.push(param.value); + } + } + } + } + + var parsedworkflows = []; + for (var key in newSubflows) { + const foundWorkflow = workflows.find( + (workflow) => workflow.id === newSubflows[key] + ); + if (foundWorkflow !== undefined && foundWorkflow !== null) { + parsedworkflows.push(foundWorkflow); + } + } + + if (parsedworkflows.length > 0) { + console.log( + "Appending subflows during export: ", + parsedworkflows.length + ); + data.subflows = parsedworkflows; + } + } + + setExportData(data) + setOpen(false) + } + + const sideloadWorkflow = (id, action, setOpen) => { + const storagewf = localStorage.getItem("workflows") const storageWorkflows = JSON.parse(storagewf) if (storageWorkflows === null || storageWorkflows === undefined || storageWorkflows.length === 0) { } else { for (var i = 0; i < storageWorkflows.length; i++) { if (storageWorkflows[i].id === id) { - if (storageWorkflows[i].image !== "") { - return + if (storageWorkflows[i].image !== "" && storageWorkflows[i].image !== undefined && storageWorkflows[i].image !== null) { + + if (action === undefined || action === null || action === "") { + console.log("RETURNING") + return + } } } } @@ -1718,8 +1827,16 @@ const Workflows = (props) => { return response.json() }) .then((responseJson) => { - if (openEdit) { - setEditing(responseJson) + if (responseJson.success !== false && responseJson.id !== undefined) { + if (action === "edit") { + setEditing(responseJson) + } else if (action === "publish") { + setPublishModalOpen(true) + setSelectedWorkflow(responseJson) + } else if (action === "export") { + exportSingleWorkflow(responseJson, setOpen) + } + } for (var i = 0; i < storageWorkflows.length; i++) { @@ -1730,15 +1847,16 @@ const Workflows = (props) => { } } - setWorkflows(storageWorkflows) - //setFilteredWorkflows(storageWorkflows) + //setWorkflows(storageWorkflows) + setFilteredWorkflows(storageWorkflows) + //setUpdate(Math.random()) }) .catch((error) => { console.log(error.toString()) }) } - const deleteWorkflow = (id) => { + const deleteWorkflow = (id, bulk) => { fetch(globalUrl + "/api/v1/workflows/" + id, { method: "DELETE", headers: { @@ -1752,15 +1870,19 @@ const Workflows = (props) => { console.log("Status not 200 for setting workflows :O!"); toast("Failed deleting workflow. Do you have access?"); } else { - toast("Deleted workflow " + id); + if (bulk !== true) { + toast("Deleted workflow " + id); + } } return response.json(); }) .then(() => { - setTimeout(() => { - getAvailableWorkflows(); - }, 1000); + if (bulk !== true) { + setTimeout(() => { + getAvailableWorkflows(); + }, 1000); + } }) .catch((error) => { toast(error.toString()); @@ -1878,7 +2000,9 @@ const Workflows = (props) => { const appGroup = getWorkflowAppgroup(data) const [triggers, subflows] = getWorkflowMeta(data) - const isDistributed = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.includes(userdata.active_org.id) + const hasSuborgs = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.length > 0 + const isDistributed = (data.parentorg_workflow !== undefined && data.parentorg_workflow !== null && data.parentorg_workflow.length > 0) //|| (data.org_id !== userdata.active_org.id && data.org_id !== undefined && data.org_id !== null && data.org_id.length > 0) + const workflowMenuButtons = ( { setAnchorEl(null); }} > + {isDistributed ? + { + navigate(`/workflows/${data.id}`) + }} + > + + Explore Workflow + + : null} { event.stopPropagation() if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") { @@ -1899,7 +2035,9 @@ const Workflows = (props) => { } else { //toast("Need to side-load workflow to be edited properly") - sideloadWorkflow(data.id, true) + sideloadWorkflow(data.id, "edit") + + toast.info("Loading full workflow for editing. Please wait...") } }} key={"change"} @@ -1909,9 +2047,11 @@ const Workflows = (props) => { { - setSelectedWorkflow(data); - setPublishModalOpen(true); + sideloadWorkflow(data.id, "publish") + + toast.info("Loading full workflow for publishing. Please wait...") }} key={"publish"} > @@ -1920,9 +2060,10 @@ const Workflows = (props) => { { - copyWorkflow(data); - setOpen(false); + duplicateWorkflow(data) + setOpen(false) }} key={"duplicate"} > @@ -1931,52 +2072,11 @@ const Workflows = (props) => { { - setExportModalOpen(true); + sideloadWorkflow(data.id, "export", setOpen) - if (data.triggers !== null && data.triggers !== undefined) { - var newSubflows = []; - for (var key in data.triggers) { - const trigger = data.triggers[key]; - - if ( - trigger.parameters !== null && - trigger.parameters !== undefined - ) { - for (var subkey in trigger.parameters) { - const param = trigger.parameters[subkey]; - if ( - param.name === "workflow" && - param.value !== data.id && - !newSubflows.includes(param.value) - ) { - newSubflows.push(param.value); - } - } - } - } - - var parsedworkflows = []; - for (var key in newSubflows) { - const foundWorkflow = workflows.find( - (workflow) => workflow.id === newSubflows[key] - ); - if (foundWorkflow !== undefined && foundWorkflow !== null) { - parsedworkflows.push(foundWorkflow); - } - } - - if (parsedworkflows.length > 0) { - console.log( - "Appending subflows during export: ", - parsedworkflows.length - ); - data.subflows = parsedworkflows; - } - } - - setExportData(data); - setOpen(false); + toast.info("Loading full workflow to be exported. Please wait...") }} key={"export"} > @@ -1985,6 +2085,7 @@ const Workflows = (props) => { { setDeleteModalOpen(true); setSelectedWorkflowId(data.id); @@ -2076,7 +2177,7 @@ const Workflows = (props) => { } return ( -
    +
    {selectedCategory !== "" ? @@ -2129,8 +2230,17 @@ const Workflows = (props) => { Edit '{data.name}' + +
    + + {isDistributed || hasSuborgs ? + + This is a parentorg-controlled workflow. + + : null}
    } placement="right"> + { } const reader = new FileReader(); + var workflowids = [] + // Waits for the read reader.addEventListener("load", (event) => { var data = reader.result; @@ -2497,7 +2609,8 @@ const Workflows = (props) => { data.org_id = userdata.active_org.id data.org = [] data.execution_org = {} - + + workflowids.push(data.id) // Actually create it setNewWorkflow( @@ -2528,6 +2641,10 @@ const Workflows = (props) => { } setLoadWorkflowsModalOpen(false); + + if (workflowids.length > 0) { + setHighlightIds(workflowids) + } }; const getWorkflowMeta = (data) => { @@ -2869,7 +2986,7 @@ const Workflows = (props) => { className={classes.datagrid} rows={rows} columns={columns} - pageSize={25} + pageSize={100} checkboxSelection autoHeight density="standard" @@ -3682,7 +3799,7 @@ const Workflows = (props) => { workflowDelay += 75 } else { return ( - + ) diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 8faab828..66579627 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -4,16 +4,16 @@ go 1.22.0 toolchain go1.22.2 -replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared require ( github.com/docker/docker v26.1.0+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.6.37 - k8s.io/api v0.30.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 + github.com/shuffle/shuffle-shared v0.6.50 + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 + k8s.io/client-go v0.30.2 ) require ( @@ -31,7 +31,7 @@ require ( github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -41,7 +41,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/frikky/kin-openapi v0.41.0 // indirect - github.com/frikky/schemaless v0.0.11 // indirect + github.com/frikky/schemaless v0.0.13 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect @@ -82,6 +82,8 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sashabaranov/go-openai v1.19.2 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 1c27a7a1..cbf4d710 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -97,6 +97,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= @@ -139,6 +141,8 @@ github.com/frikky/schemaless v0.0.9 h1:RzNLPkJq5c4nlm5iLiTndFcbeQxdMGJIj266wSGt2 github.com/frikky/schemaless v0.0.9/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= github.com/frikky/schemaless v0.0.11 h1:c4r6CJX30XI+SoJdT9RlUd9qYSQlx6hvwGRtsypu+uM= github.com/frikky/schemaless v0.0.11/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= +github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ= +github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -385,6 +389,10 @@ github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6 github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shuffle/shuffle-shared v0.6.16 h1:dQBDRmb2Wgl3pEuewqjDvN6v6nUKr+1EvGSEja9zG6s= @@ -395,6 +403,8 @@ github.com/shuffle/shuffle-shared v0.6.27 h1:q4qZD6bGZFIvZ5Y10unGr3N3rZ7OryWyvva github.com/shuffle/shuffle-shared v0.6.27/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= github.com/shuffle/shuffle-shared v0.6.37 h1:IB8tJqubJmJwwpLYbXNMVWck5jgVO9SKcKo/BBu+wTc= github.com/shuffle/shuffle-shared v0.6.37/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= +github.com/shuffle/shuffle-shared v0.6.50 h1:MBeGAiBNkw9Eg+3YTJIlBOuskWntGvT0uefFUYOBhbY= +github.com/shuffle/shuffle-shared v0.6.50/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -908,10 +918,16 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index ce92455d..bb89a83f 100755 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -1452,7 +1452,7 @@ func main() { if isKubernetes == "true" { clientset, _, err := shuffle.GetKubernetesClient() if err != nil { - log.Printf("[ERROR] Error getting kubernetes client:", err) + log.Printf("[ERROR] Error getting kubernetes client: %s", err) os.Exit(1) } diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index ace7c908..1aa83b26 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1897,87 +1897,87 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar { } func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { -if request.Body == nil { - log.Printf("[WARNING] (2) No body in request for workflowqueue") - resp.WriteHeader(http.StatusBadRequest) - return -} - -defer request.Body.Close() -body, err := ioutil.ReadAll(request.Body) -if err != nil { - log.Printf("[WARNING] (3) Failed reading body for workflowqueue") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return -} - -var actionResult shuffle.ActionResult -err = json.Unmarshal(body, &actionResult) -if err != nil { - log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err) - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - //return -} - -if len(actionResult.ExecutionId) == 0 { - log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body)) - resp.WriteHeader(400) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`))) - return -} - -// 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database -// 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit -// 3. Add to and update actionResult in workflowExecution -// 4. Push to db -// IF FAIL: Set executionstatus: abort or cancel -ctx := context.Background() -workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) -if err != nil { - log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) - resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) - return -} - -if workflowExecution.Authorization != actionResult.Authorization { - log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization) - resp.WriteHeader(403) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`))) - return -} - -if workflowExecution.Status == "FINISHED" { - log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode))) - return -} - -if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { - log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status))) - return -} - -retries := 0 -retry, retriesok := request.URL.Query()["retries"] -if retriesok && len(retry) > 0 { - val, err := strconv.Atoi(retry[0]) - if err == nil { - retries = val + if request.Body == nil { + log.Printf("[WARNING] (2) No body in request for workflowqueue") + resp.WriteHeader(http.StatusBadRequest) + return } -} -log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries) + defer request.Body.Close() + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[WARNING] (3) Failed reading body for workflowqueue") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } -//results = append(results, actionResult) -//log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) -//log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId) -runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) + var actionResult shuffle.ActionResult + err = json.Unmarshal(body, &actionResult) + if err != nil { + log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err) + //resp.WriteHeader(401) + //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + //return + } + + if len(actionResult.ExecutionId) == 0 { + log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body)) + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`))) + return + } + + // 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database + // 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit + // 3. Add to and update actionResult in workflowExecution + // 4. Push to db + // IF FAIL: Set executionstatus: abort or cancel + ctx := context.Background() + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) + return + } + + if workflowExecution.Authorization != actionResult.Authorization { + log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization) + resp.WriteHeader(403) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`))) + return + } + + if workflowExecution.Status == "FINISHED" { + log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode))) + return + } + + if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { + log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status))) + return + } + + retries := 0 + retry, retriesok := request.URL.Query()["retries"] + if retriesok && len(retry) > 0 { + val, err := strconv.Atoi(retry[0]) + if err == nil { + retries = val + } + } + + log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries) + + //results = append(results, actionResult) + //log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) + //log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId) + runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) } // Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times @@ -2090,7 +2090,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl */ } } - + if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) //result(s)", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results))