diff --git a/.env b/.env old mode 100644 new mode 100755 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md old mode 100644 new mode 100755 diff --git a/.github/install-guide.html b/.github/install-guide.html old mode 100644 new mode 100755 diff --git a/.github/install-guide.md b/.github/install-guide.md old mode 100644 new mode 100755 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml old mode 100644 new mode 100755 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/snyk-container-analysis.yml b/.github/workflows/snyk-container-analysis.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/snyk-infrastructure-analysis.yml b/.github/workflows/snyk-infrastructure-analysis.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/SECURITY.md b/SECURITY.md old mode 100644 new mode 100755 diff --git a/backend/Dockerfile b/backend/Dockerfile old mode 100644 new mode 100755 diff --git a/backend/README.md b/backend/README.md old mode 100644 new mode 100755 diff --git a/backend/app_gen/LICENSE b/backend/app_gen/LICENSE old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi-parsers/misp.py b/backend/app_gen/openapi-parsers/misp.py old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi-parsers/swimlane.py b/backend/app_gen/openapi-parsers/swimlane.py old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi/README.md b/backend/app_gen/openapi/README.md old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi/baseline/Dockerfile b/backend/app_gen/openapi/baseline/Dockerfile old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi/baseline/requirements.txt b/backend/app_gen/openapi/baseline/requirements.txt old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi/test.go b/backend/app_gen/openapi/test.go old mode 100644 new mode 100755 diff --git a/backend/app_gen/openapi/testGCP.go b/backend/app_gen/openapi/testGCP.go old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/README.md b/backend/app_gen/python-lib/README.md old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/baseline/Dockerfile b/backend/app_gen/python-lib/baseline/Dockerfile old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/baseline/docker-compose.yml b/backend/app_gen/python-lib/baseline/docker-compose.yml old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/baseline/env.txt b/backend/app_gen/python-lib/baseline/env.txt old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/baseline/requirements.txt b/backend/app_gen/python-lib/baseline/requirements.txt old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/generator.py b/backend/app_gen/python-lib/generator.py old mode 100644 new mode 100755 diff --git a/backend/app_gen/python-lib/requirements.txt b/backend/app_gen/python-lib/requirements.txt old mode 100644 new mode 100755 diff --git a/backend/app_sdk/Dockerfile b/backend/app_sdk/Dockerfile old mode 100644 new mode 100755 diff --git a/backend/app_sdk/Dockerfile_blackarch b/backend/app_sdk/Dockerfile_blackarch old mode 100644 new mode 100755 diff --git a/backend/app_sdk/Dockerfile_kali b/backend/app_sdk/Dockerfile_kali old mode 100644 new mode 100755 diff --git a/backend/app_sdk/LICENSE b/backend/app_sdk/LICENSE old mode 100644 new mode 100755 diff --git a/backend/app_sdk/README.md b/backend/app_sdk/README.md old mode 100644 new mode 100755 diff --git a/backend/app_sdk/__init__.py b/backend/app_sdk/__init__.py old mode 100644 new mode 100755 diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py old mode 100644 new mode 100755 diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh old mode 100644 new mode 100755 diff --git a/backend/app_sdk/requirements.txt b/backend/app_sdk/requirements.txt old mode 100644 new mode 100755 diff --git a/backend/build.sh b/backend/build.sh old mode 100644 new mode 100755 diff --git a/backend/database/opensearch/docker-compose.yml b/backend/database/opensearch/docker-compose.yml old mode 100644 new mode 100755 diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go old mode 100644 new mode 100755 index 62c5878b..391386d5 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -698,7 +698,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) { // REBUILDS THE APP if len(img.ID) == 0 { if len(img2.ID) == 0 { - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 0) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 0, 0) log.Printf("[INFO] Getting workflowapps for a rebuild. Got %d with err %#v", len(workflowapps), err) if err == nil { imageName := "" diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod old mode 100644 new mode 100755 index 16f22e0f..e89d5686 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -7,44 +7,29 @@ replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi require ( - cloud.google.com/go v0.75.0 cloud.google.com/go/datastore v1.4.0 cloud.google.com/go/pubsub v1.3.1 cloud.google.com/go/storage v1.12.0 - github.com/Masterminds/semver v1.5.0 // indirect - github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b // indirect - github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect github.com/basgys/goxml2json v1.1.0 - github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v20.10.3-0.20210216175712-646072ed6524+incompatible - github.com/docker/go-connections v0.4.0 - github.com/docker/go-units v0.4.0 // indirect - github.com/elastic/go-elasticsearch/v7 v7.13.1 // indirect - github.com/frikky/kin-openapi v0.39.0 - github.com/frikky/shuffle-shared v0.1.15 + github.com/frikky/kin-openapi v0.41.0 github.com/fsouza/go-dockerclient v1.7.2 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 - github.com/google/go-github/v28 v28.1.1 - github.com/gorilla/handlers v1.4.2 // indirect github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.0.12 - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect - github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/opensearch-project/opensearch-go v1.1.0 // indirect + github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect github.com/satori/go.uuid v1.2.0 github.com/shuffle/shuffle-shared v0.1.15 - go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 google.golang.org/api v0.36.0 google.golang.org/appengine v1.6.7 - google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595 google.golang.org/grpc v1.34.1 gopkg.in/src-d/go-git.v4 v4.13.1 - gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/backend/go-app/main.go b/backend/go-app/main.go old mode 100644 new mode 100755 index cdece1bb..356f16e0 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3028,7 +3028,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) { if test.Editing && len(user.Id) > 0 { // Quick verification test ctx := context.Background() - app, err := shuffle.GetApp(ctx, test.Id, user) + app, err := shuffle.GetApp(ctx, test.Id, user, false) if err != nil { log.Printf("[WARNING] Error getting app when editing: %s", app.Name) resp.WriteHeader(401) @@ -4019,7 +4019,7 @@ func runInitEs(ctx context.Context) { // Getting apps to see if we should initialize a test // FIXME: Isn't this a little backwards? - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) log.Printf("[INFO] Getting and validating workflowapps. Got %d with err %#v", len(workflowapps), err) // accept any certificate (might be useful for testing) @@ -4671,7 +4671,7 @@ func runInit(ctx context.Context) { } // Getting apps to see if we should initialize a test - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000,0 ) log.Printf("[INFO] Getting and validating workflowapps. Got %d with err %s", len(workflowapps), err) if err != nil && len(workflowapps) == 0 { log.Printf("[WARNING] Failed getting apps (runInit): %s", err) @@ -5286,7 +5286,7 @@ func migrateDatabase(resp http.ResponseWriter, request *http.Request) { log.Printf("[DEBUG] Found %d workflows(s) to be migrated", len(workflows)) } - apps, err := shuffle.GetAllWorkflowApps(ctx, 0) + apps, err := shuffle.GetAllWorkflowApps(ctx, 0, 0) if err != nil { log.Printf("[ERROR] Failed getting apps: %#v", err) } else { @@ -5729,6 +5729,7 @@ func initHandlers() { r.HandleFunc("/api/v1/workflows/{key}", deleteWorkflow).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}", shuffle.SaveWorkflow).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}", shuffle.GetSpecificWorkflow).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/revisions", shuffle.GetWorkflowRevisions).Methods("GET", "OPTIONS") // Triggers r.HandleFunc("/api/v1/hooks/new", shuffle.HandleNewHook).Methods("POST", "OPTIONS") diff --git a/backend/go-app/main_test.go b/backend/go-app/main_test.go old mode 100644 new mode 100755 diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go old mode 100644 new mode 100755 index 7078110e..05de34a2 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -577,7 +577,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque //setWorkflowqueuetest(id) ctx := context.Background() - executionRequests, err := shuffle.GetWorkflowQueue(ctx, id) + executionRequests, err := shuffle.GetWorkflowQueue(ctx, id, 10) if err != nil { log.Printf("(1) Failed reading body for workflowqueue: %s", err) resp.WriteHeader(401) @@ -676,7 +676,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - executionRequests, err := shuffle.GetWorkflowQueue(ctx, id) + executionRequests, err := shuffle.GetWorkflowQueue(ctx, id, 10) if err != nil { // Skipping as this comes up over and over //log.Printf("(2) Failed reading body for workflowqueue: %s", err) @@ -782,7 +782,8 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { //log.Printf("Actionresult unmarshal: %s", string(body)) log.Printf("[DEBUG] Got workflow result from %s of length %d", request.RemoteAddr, len(body)) - err = shuffle.ValidateNewWorkerExecution(body) + ctx := context.Background() + err = shuffle.ValidateNewWorkerExecution(ctx, body) if err == nil { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "success"}`))) @@ -808,7 +809,6 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // 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] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, err) @@ -910,7 +910,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } //log.Printf("BASE LENGTH: %d", len(workflowExecution.Results)) - workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false) + workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false, 0) if err != nil { log.Printf("[ERROR] Failed running of parsedexecution: %s", err) resp.WriteHeader(401) @@ -1514,7 +1514,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request } } - childNodes := shuffle.FindChildNodes(workflowExecution, workflowExecution.Start) + childNodes := shuffle.FindChildNodes(workflowExecution, workflowExecution.Start, []string{}, []string{}) topic := "workflows" startFound := false @@ -1561,14 +1561,14 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request newFields := []shuffle.AuthenticationStore{} for _, field := range curAuth.Fields { parsedKey := fmt.Sprintf("%s_%d_%s_%s", curAuth.OrgId, curAuth.Created, curAuth.Label, field.Key) - newValue, err := shuffle.HandleKeyDecryption(field.Value, parsedKey) + newValue, err := shuffle.HandleKeyDecryption([]byte(field.Value), parsedKey) if err != nil { log.Printf("[WARNING] Failed decryption for %s: %s", field.Key, err) setField = false break } - field.Value = newValue + field.Value = string(newValue) newFields = append(newFields, field) } @@ -2655,7 +2655,7 @@ func getWorkflowApps(resp http.ResponseWriter, request *http.Request) { return } - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) if err != nil { log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err) resp.WriteHeader(401) @@ -2753,7 +2753,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) { // FIXME - continue the search here with github repos etc. // Caching might be smart :D ctx := context.Background() - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) if err != nil { log.Printf("Error: Failed getting workflowapps: %s", err) resp.WriteHeader(401) @@ -3023,7 +3023,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string) error { ctx := context.Background() - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) appCounter := 0 if err != nil { log.Printf("Failed to get existing generated apps") @@ -3316,7 +3316,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - allapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) + allapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) if err != nil { log.Printf("Failed getting apps to verify: %s", err) resp.WriteHeader(401) @@ -3582,10 +3582,14 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) { time.Sleep(2 * time.Second) log.Printf("[INFO] Starting validation of execution %s", workflowExecution.ExecutionId) - returnBytes := shuffle.HandleRetValidation(ctx, workflowExecution) + returnBody := shuffle.HandleRetValidation(ctx, workflowExecution, 1) + returnBytes, err := json.Marshal(returnBody) + if err != nil { + log.Printf("[ERROR] Failed to marshal retStruct in single execution: %s", err) + } resp.WriteHeader(200) - resp.Write(returnBytes) + resp.Write([]byte(returnBytes)) } // Onlyname is used to @@ -3766,7 +3770,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os. } if len(allapps) == 0 { - allapps, err = shuffle.GetAllWorkflowApps(ctx, 0) + allapps, err = shuffle.GetAllWorkflowApps(ctx, 0, 0) if err != nil { log.Printf("[WARNING] Failed getting apps to verify: %s", err) continue diff --git a/backend/tests/cache.sh b/backend/tests/cache.sh old mode 100644 new mode 100755 diff --git a/backend/tests/cleanup.sh b/backend/tests/cleanup.sh old mode 100644 new mode 100755 diff --git a/backend/tests/dockerpull.sh b/backend/tests/dockerpull.sh old mode 100644 new mode 100755 diff --git a/backend/tests/execute.sh b/backend/tests/execute.sh old mode 100644 new mode 100755 diff --git a/backend/tests/file_download.sh b/backend/tests/file_download.sh old mode 100644 new mode 100755 diff --git a/backend/tests/forparser.py b/backend/tests/forparser.py old mode 100644 new mode 100755 diff --git a/backend/tests/hooks.sh b/backend/tests/hooks.sh old mode 100644 new mode 100755 diff --git a/backend/tests/hotload.sh b/backend/tests/hotload.sh old mode 100644 new mode 100755 diff --git a/backend/tests/list_files.sh b/backend/tests/list_files.sh old mode 100644 new mode 100755 diff --git a/backend/tests/migrate_db.sh b/backend/tests/migrate_db.sh old mode 100644 new mode 100755 diff --git a/backend/tests/run_function.py b/backend/tests/run_function.py old mode 100644 new mode 100755 diff --git a/backend/tests/scheduleapps.sh b/backend/tests/scheduleapps.sh old mode 100644 new mode 100755 diff --git a/backend/tests/schedules.sh b/backend/tests/schedules.sh old mode 100644 new mode 100755 diff --git a/backend/tests/sendmail.sh b/backend/tests/sendmail.sh old mode 100644 new mode 100755 diff --git a/backend/tests/testWorkflows.sh b/backend/tests/testWorkflows.sh old mode 100644 new mode 100755 diff --git a/backend/tests/test_wrappers.py b/backend/tests/test_wrappers.py old mode 100644 new mode 100755 diff --git a/backend/tests/triggers.sh b/backend/tests/triggers.sh old mode 100644 new mode 100755 diff --git a/backend/tests/users.sh b/backend/tests/users.sh old mode 100644 new mode 100755 diff --git a/backend/tests/validate_app_values.sh b/backend/tests/validate_app_values.sh old mode 100644 new mode 100755 diff --git a/backend/tests/websocket.sh b/backend/tests/websocket.sh old mode 100644 new mode 100755 diff --git a/backend/tests/workflowdata.json b/backend/tests/workflowdata.json old mode 100644 new mode 100755 diff --git a/backend/tests/workflowresults.sh b/backend/tests/workflowresults.sh old mode 100644 new mode 100755 diff --git a/backend/tests/workflows.sh b/backend/tests/workflows.sh old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index 88fe59f2..3cb35784 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,7 +66,7 @@ services: - CLEANUP=${SHUFFLE_CONTAINER_AUTO_CLEANUP} restart: unless-stopped opensearch: - image: opensearchproject/opensearch:1.1.0 + image: opensearchproject/opensearch:2.5.0 hostname: shuffle-opensearch container_name: shuffle-opensearch environment: diff --git a/frontend/Dockerfile b/frontend/Dockerfile old mode 100644 new mode 100755 diff --git a/frontend/README.md b/frontend/README.md old mode 100644 new mode 100755 diff --git a/frontend/build.sh b/frontend/build.sh old mode 100644 new mode 100755 diff --git a/frontend/certs/certreq.csr b/frontend/certs/certreq.csr old mode 100644 new mode 100755 diff --git a/frontend/certs/fullchain.pem b/frontend/certs/fullchain.pem old mode 100644 new mode 100755 diff --git a/frontend/certs/privkey.pem b/frontend/certs/privkey.pem old mode 100644 new mode 100755 diff --git a/frontend/confd/conf.d/nginx.conf.toml b/frontend/confd/conf.d/nginx.conf.toml old mode 100644 new mode 100755 diff --git a/frontend/confd/templates/nginx.conf b/frontend/confd/templates/nginx.conf old mode 100644 new mode 100755 diff --git a/frontend/package.json b/frontend/package.json old mode 100644 new mode 100755 diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico old mode 100644 new mode 100755 diff --git a/frontend/public/images/Shuffle_logo.png b/frontend/public/images/Shuffle_logo.png old mode 100644 new mode 100755 diff --git a/frontend/public/index.html b/frontend/public/index.html old mode 100644 new mode 100755 diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json old mode 100644 new mode 100755 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/__test__/appdata.js b/frontend/src/__test__/appdata.js old mode 100644 new mode 100755 diff --git a/frontend/src/__test__/environmentdata.js b/frontend/src/__test__/environmentdata.js old mode 100644 new mode 100755 diff --git a/frontend/src/__test__/scheduledata.js b/frontend/src/__test__/scheduledata.js old mode 100644 new mode 100755 diff --git a/frontend/src/__test__/webhookdata.js b/frontend/src/__test__/webhookdata.js old mode 100644 new mode 100755 diff --git a/frontend/src/__test__/workflowdata.js b/frontend/src/__test__/workflowdata.js old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/bag.svg b/frontend/src/assets/img/bag.svg old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/book.svg b/frontend/src/assets/img/book.svg old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/github_phishing_email.png b/frontend/src/assets/img/github_phishing_email.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/github_shuffle_img.png b/frontend/src/assets/img/github_shuffle_img.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/icpl_logo.png b/frontend/src/assets/img/icpl_logo.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/kafka.png b/frontend/src/assets/img/kafka.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/logo.png b/frontend/src/assets/img/logo.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/logo.svg b/frontend/src/assets/img/logo.svg old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/logo.webp b/frontend/src/assets/img/logo.webp old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/mobile.svg b/frontend/src/assets/img/mobile.svg old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/shuffle_adminaccount.png b/frontend/src/assets/img/shuffle_adminaccount.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/shuffle_architecture.png b/frontend/src/assets/img/shuffle_architecture.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/shuffle_webhook.png b/frontend/src/assets/img/shuffle_webhook.png old mode 100644 new mode 100755 diff --git a/frontend/src/assets/img/webhook.png b/frontend/src/assets/img/webhook.png old mode 100644 new mode 100755 diff --git a/frontend/src/charts.js b/frontend/src/charts.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/AlertPopup.js b/frontend/src/components/AlertPopup.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/AlertTemplate.js b/frontend/src/components/AlertTemplate.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx old mode 100644 new mode 100755 index 43b93d33..b7b9f2b0 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -1,267 +1,451 @@ -import React, {useState} from 'react'; +import React, { useState, useEffect } from "react"; +import { useInterval } from "react-powerhooks"; -import { InputAdornment, Tooltip, TextField, CircularProgress, ButtonGroup, Button, Avatar, ListItemAvatar, Typography, List, ListItem, ListItemText} from '@material-ui/core'; -import {FavoriteBorder as FavoriteBorderIcon} from '@material-ui/icons'; +import { + InputAdornment, + Tooltip, + TextField, + CircularProgress, + ButtonGroup, + Button, + Avatar, + ListItemAvatar, + Typography, + List, + ListItem, + ListItemText, + Fade, +} from "@material-ui/core"; +import { + FavoriteBorder as FavoriteBorderIcon, + Error as ErrorIcon, + CheckCircleRounded as CheckCircleRoundedIcon, +} from "@mui/icons-material"; import { FixName } from "../views/Apps.jsx"; +import aa from 'search-insights' // Handles workflow updates on first open to highlight the issues of the workflow // Variables -// Action (exists, missing fields) +// Action (exists, missing fields) // Action auth // Triggers // // Specifically used for UNSAVED workflows only? const ConfigureWorkflow = (props) => { - const { globalUrl, theme, workflow, appAuthentication, setSelectedAction, setAuthenticationModalOpen, setSelectedApp, apps, selectedAction,setConfigureWorkflowModalOpen, saveWorkflow, newWebhook, submitSchedule, referenceUrl, isCloud, setAuthenticationType, alert, } = props - const [requiredActions, setRequiredActions] = React.useState([]) - const [requiredVariables, setRequiredVariables] = React.useState([]) - const [requiredTriggers, setRequiredTriggers] = React.useState([]) - const [previousAuth, setPreviousAuth] = React.useState(appAuthentication) - const [firstLoad, setFirstLoad] = React.useState("") - const [itemChanged, setItemChanged] = React.useState(false) - var finished = false + const { + userdata, + globalUrl, + theme, + workflow, + appAuthentication, + setSelectedAction, + setAuthenticationModalOpen, + setSelectedApp, + apps, + selectedAction, + setConfigureWorkflowModalOpen, + saveWorkflow, + newWebhook, + submitSchedule, + referenceUrl, + isCloud, + setAuthenticationType, + alert, + showTriggers, + workflowExecutions, + getWorkflowExecution, + } = props; - if (workflow === undefined || workflow === null) { - return null - } + const [requiredActions, setRequiredActions] = React.useState([]); + const [requiredVariables, setRequiredVariables] = React.useState([]); + const [requiredTriggers, setRequiredTriggers] = React.useState([]); + const [previousAuth, setPreviousAuth] = React.useState(appAuthentication); + const [itemChanged, setItemChanged] = React.useState(false); + const [firstLoad, setFirstLoad] = React.useState(""); + const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false); - if (apps === undefined || apps === null) { - return null - } + const [checkStarted, setCheckStarted] = React.useState(false); - if (appAuthentication === undefined || appAuthentication === null) { - return null - } - - const getApp = (actionId, appId) => { - fetch(globalUrl+"/api/v1/apps/"+appId+"/config?openapi=false", { - headers: { - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - //alert.success("Successfully GOT app "+appId) + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) { + const paramkey = workflow.id + getWorkflowExecution(paramkey) } else { - alert.error("Failed getting app") + console.log("Executions or getWorkflowExecutions not defined") } + }, + }); - return response.json() - }) - .then((responseJson) => { - console.log("ACTION: ", responseJson) - if (responseJson.actions !== undefined && responseJson.actions !== null) { - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - if (firstLoad.length === 0 || firstLoad !== workflow.id) { - if (finished) { - setConfigureWorkflowModalOpen(false) - return null + // ONLY when component is being unloaded, run stop() function + // This is to prevent the interval from running when the component is not being used + + /* + useEffect(() => { + return () => { + stop() } + }, []) + */ - if (apps === undefined || apps === null || apps.length === 0) { - console.log("No apps loaded: ", apps) - setConfigureWorkflowModalOpen(false) - return null - } + // Where is this from? + if (workflow === undefined || workflow === null) { + return null; + } - setFirstLoad(workflow.id) - const newactions = [] - for (var key in workflow.actions) { - const action = workflow.actions[key] - var newaction = { - "large_image": action.large_image, - "app_name": action.app_name, - "app_version": action.app_version, - "activation_done": false, - "must_activate": false, - "must_authenticate": false, - "auth_done": false, - "action_ids": [], - "action": action, - "app": {}, - } + if (apps === undefined || apps === null) { + return null; + } - const app = apps.find(app => app.name === action.app_name && (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version)))) - if (app === undefined || app === null) { - console.log("App not found: ", action.app_name) + if (appAuthentication === undefined || appAuthentication === null) { + return null; + } - newaction.must_activate = true - } else { - if (action.authentication_id === "" && app.authentication.required === true) { - // Check if configuration is filled or not - var filled = true - for (var key in action.parameters) { - if (action.parameters[key].configuration) { - //console.log("Found config: ", action.parameters[key]) - if (action.parameters[key].value === null || action.parameters[key].value.length === 0) { - filled = false - break + const getApp = (actionId, appId) => { + fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //alert.success("Successfully GOT app "+appId) + } else { + alert.error("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("ACTION: ", responseJson); + if ( + responseJson.actions !== undefined && + responseJson.actions !== null + ) { + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + if (firstLoad.length === 0 || firstLoad !== workflow.id) { + if (apps === undefined || apps === null || apps.length === 0) { + console.log("No apps loaded: ", apps); + setConfigureWorkflowModalOpen(false); + return null; + } + + setFirstLoad(workflow.id) + const newactions = []; + for (let [key, keyval] in Object.entries(workflow.actions)) { + const action = workflow.actions[key]; + var newaction = { + large_image: action.large_image, + app_name: action.app_name, + app_version: action.app_version, + activation_done: false, + must_activate: false, + must_authenticate: false, + auth_done: false, + action_ids: [], + action: action, + update_version: action.app_version, + app: {}, + steps: [], + show_steps: false, + } + + //console.log("Action: ", key, keyval) + + const app = apps.find((app) => + app.id === action.app_id || + (app.name === action.app_name && + (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version)))) + ) + + //console.log("FOUND APP: ", app) + + if (app === undefined || app === null) { + const subapp = apps.find(app => app.name === action.app_name) + if (subapp !== undefined && subapp !== null) { + newaction.update_version = "1.1.0" + } + + newaction.must_activate = true; + newaction.steps.push({ + "title": "Activate app", + "type": "activate", + "required": true, + }) + } else { + if (action.authentication_id === "" && app.authentication.required === true && action.parameters !== undefined && action.parameters !== null) { + // Check if configuration is filled or not + var filled = true; + for (let [key,keyval] in Object.entries(action.parameters)) { + if (action.parameters[key].configuration) { + //console.log("Found config: ", action.parameters[key]) + if ( + action.parameters[key].value === null || + action.parameters[key].value.length === 0 + ) { + filled = false; + break; + } + } + } + + newaction.steps.push({ + "title": "Authenticate app", + "type": "authenticate", + "required": true, + }) + + if (!filled) { + newaction.must_authenticate = true; + newaction.action_ids.push(action.id); + } + } else if (action.authentication_id !== "" && app.authentication.required === true) { + console.log("Should verify authentication ID ", action.authentication_id) + + } + + newaction.app = app; + } + + if ( + action.errors !== undefined && + action.errors !== null && + action.errors.length > 0 + ) { + //console.log("Node has errors!: ", action.errors) + } + + if (newaction.must_authenticate) { + var authenticationOptions = []; + for (let [key,keyval] in Object.entries(appAuthentication)) { + const auth = appAuthentication[key]; + if (auth.app.name === app.name && auth.active) { + //console.log("Found auth: ", auth) + authenticationOptions.push(auth); + newaction.authenticationId = auth.id; + break; + } + } + + if ( + newaction.authenticationId === null || + newaction.authenticationId === undefined || + newaction.authenticationId.length === "" + ) { + //console.log("FAILED to authenticate node!") + + if ( + newactions.find( + (tmpaction) => + tmpaction.app_id === newaction.app_id && + tmpaction.app_name === newaction.app_name + ) !== undefined + ) { + //console.log("Action already found."); + } else { + newactions.push(newaction); + } + } else { + //console.log("Skipping node as it's already authenticated.") + newaction.authentication = authenticationOptions; + workflow.actions[key] = newaction; + } + } else if (newaction.must_activate) { + if ( + newactions.find( + (tmpaction) => + tmpaction.app_id === newaction.app_id && + tmpaction.app_name === newaction.app_name + ) !== undefined + ) { + console.log("Action already found."); + } else { + newactions.push(newaction); + } + } + } + + if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length !== 0) { + for (let [key,keyval] in Object.entries(workflow.workflow_variables)) { + const variable = workflow.workflow_variables[key]; + if (variable.value === undefined || variable.value === undefined || variable.value.length === 0) { + variable.value = ""; + requiredVariables.push(variable); + } + + variable.index = key; + } + } + + if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length !== 0) { + for (let [key,keyval] in Object.entries(workflow.triggers)) { + var trigger = workflow.triggers[key]; + trigger.index = key; + + if (trigger.trigger_type === "WEBHOOK") { + 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) + + // Adding webhook to start of it + if (foundindex >= 0) { + const tmpsteps = newactions[foundindex].steps + newactions[foundindex].steps = [ + { + "title": "Configure Webhook", + "type": "webhook", + "required": true, } + ] + + for (let [subkey,subkeyval] in Object.entries(tmpsteps)) { + newactions[foundindex].steps.push(tmpsteps[subkey]) } - } - - if (!filled) { - newaction.must_authenticate = true - newaction.action_ids.push(action.id) - } - } - - newaction.app = app - } - - if (action.errors !== undefined && action.errors !== null && action.errors.length > 0) { - //console.log("Node has errors!: ", action.errors) - } - - if (newaction.must_authenticate) { - var authenticationOptions = [] - for (var key in appAuthentication) { - const auth = appAuthentication[key] - if (auth.app.name === app.name && auth.active) { - //console.log("Found auth: ", auth) - authenticationOptions.push(auth) - newaction.authenticationId = auth.id - break - } - } - - if (newaction.authenticationId === null || newaction.authenticationId === undefined || newaction.authenticationId.length === "") { - //console.log("FAILED to authenticate node!") - - if (newactions.find(tmpaction => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name) !== undefined) { - console.log("Action already found.") - } else { - newactions.push(newaction) - } - } else { - //console.log("Skipping node as it's already authenticated.") - newaction.authentication = authenticationOptions - workflow.actions[key] = newaction - } - } else if (newaction.must_activate) { - - if (newactions.find(tmpaction => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name) !== undefined) { - console.log("Action already found.") - } else { - newactions.push(newaction) - } - } - } - - for (var key in workflow.workflow_variables) { - const variable = workflow.workflow_variables[key] - if (variable.value === undefined || variable.value === undefined || variable.value.length < 2) { - variable.value = "" - variable.index = key - requiredVariables.push(variable) - } - } - - for (var key in workflow.triggers) { - var trigger = workflow.triggers[key] - trigger.index = key - - if (trigger.status === "running") { - continue - } - - if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") { - continue - } - requiredTriggers.push(trigger) - } + newactions[foundindex].show_steps = true - if (requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0) { - setConfigureWorkflowModalOpen(false) - } + console.log("CHANGED ACTION: ", newactions[foundindex]) + //console.log("Index: ", newactions[foundindex]) - setRequiredTriggers(requiredTriggers) - setRequiredVariables(requiredVariables) - setRequiredActions(newactions) - } - - if (appAuthentication.length !== previousAuth.length) { - var newactions = [] - for (var actionkey in requiredActions) { - var newaction = requiredActions[actionkey] - const app = newaction.app - - for (var key in appAuthentication) { - const auth = appAuthentication[key] - if (auth.app.name === app.name && auth.active) { - newaction.auth_done = true - break + continue + } } } - newactions.push(newaction) - } + if (trigger.status === "running") { + continue; + } - setRequiredActions(newactions) - setPreviousAuth(appAuthentication) - // Set auth done to true - //"auth_done": false + if ( + trigger.trigger_type === "SUBFLOW" || + trigger.trigger_type === "USERINPUT" + ) { + continue; + } + + requiredTriggers.push(trigger); + } +} + + if ( + requiredTriggers.length === 0 && + requiredVariables.length === 0 && + newactions.length === 0 + ) { + setConfigureWorkflowModalOpen(false); + } + + setRequiredTriggers(requiredTriggers); + setRequiredVariables(requiredVariables); + setRequiredActions(newactions); } - const TriggerSection = (props) => { - const {trigger} = props + if (appAuthentication !== undefined && previousAuth !== undefined && appAuthentication.length !== previousAuth.length) { + var newactions = [] + for (let [actionkey, actionkeyval] in Object.entries(requiredActions)) { + var newaction = requiredActions[actionkey]; + const app = newaction.app; - return ( - - - - {trigger.label} - - - - {trigger.trigger_type === "WEBHOOK" && trigger.status !== "running" ? - - : - trigger.trigger_type === "SCHEDULE" && trigger.status !== "running" ? - - : - null} - {/* + newactions.push(newaction); + } + + setRequiredActions(newactions); + setPreviousAuth(appAuthentication); + // Set auth done to true + //"auth_done": false + } + + const TriggerSection = (props) => { + const { trigger } = props + + return ( + + + + {trigger.label} + + + + {trigger.trigger_type === "WEBHOOK" && trigger.status !== "running" ? ( + + ) : trigger.trigger_type === "SCHEDULE" && + trigger.status !== "running" ? ( + + ) : null} + {/* { style={{}} /> */} - - ) - } + + ); + }; - const VariableSection = (props) => { - const {variable} = props + const VariableSection = (props) => { + const { variable } = props; - //Name: {variable.name} - {variable.value}. - return ( - - - - - - - - - - ) - }} - fullWidth - color="primary" - type={"text"} - placeholder={`New value for ${variable.name}`} - onChange={(event) => { - console.log("NEW VALUE ON INDEX", variable.index, variable.value) - }} - onBlur={(event) => { - workflow.workflow_variables[variable.index].value = event.target.value - }} - /> + //Name: {variable.name} - {variable.value}. + return ( + + + + + + + + , + }} + fullWidth + color="primary" + type={"text"} + placeholder={`New value for ${variable.name}`} + onChange={(event) => { + console.log( + "NEW VALUE ON INDEX", + variable.index, + variable.value + ); + }} + onBlur={(event) => { + workflow.workflow_variables[variable.index].value = + event.target.value; + }} + /> + } + style={{}} + /> + + ); + }; + + const activateApp = (app_id, app_name, app_version) => { + + if (aa !== undefined) { + aa('init', { + appId: "JNSS5CFDZZ", + apiKey: "db08e40265e2941b9a7d8f644b6e5240", + }) + + const timestamp = new Date().getTime() + aa('sendEvents', [ + { + eventType: 'conversion', + eventName: 'Public App Activated', + index: 'appsearch', + objectIDs: [app_id], + timestamp: timestamp, + userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, + } + ]) + } + + fetch( + `${globalUrl}/api/v1/apps/${app_id}/activate?app_name=${app_name}&app_version=${app_version}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + //window.location.pathname = "/search" + //alert.error("Failed to find this app. Is it public?") + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + alert.error("Failed to activate the app: "+responseJson.reason); + } else { + alert.error("Failed to activate the app"); } - style={{}} - /> - + } else { + alert.success("App activated for your organization!"); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + + + const AppSection = (props) => { + const { action } = props; + + return ( + + {/* + + + {action.app_name} + + + + */} + {action.must_authenticate ? + + : null} + {action.update_version !== action.app_version ? + + : + action.must_activate ? + + : + null + } + + ); + } + + // Based on the color here. Default: #f86a3e + //backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor, + + const BoxHighlight = (props) => { + const {data, appname, appinfo, index, activeStep, setActiveStep, finished, } = props + + const [hovered, setHovered] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + // This kind of just works for new workflows.. + // What if we try many times? + + var webhook = { + "name": "Testhook", + "description": `A Webhook Trigger has been started and is ready to receive events from ${appname}. Click to copy the URL to send events to.`, + "url": "", + } + + useEffect(() => { + if (data.type === "webhook" && !finished) { + if (!checkStarted) { + setCheckStarted(true) + start() + } + } + }, []) + + // Load webhook docs from the app itself (Wazuh) + // Add a "sample" for what the event is supposed to look like + // Have a listener for when ACTUALLY is received + // INJECT the URL into the documentation when loading it in + // How can we load it in? Should we just use the app name & get docs -> parse? + if (data.type === "webhook" && workflow.triggers !== undefined && workflow.triggers !== null) { + //console.log("Find webhook in the workflow!") + for (var key in workflow.triggers) { + if (workflow.triggers[key].trigger_type !== "WEBHOOK") { + continue + } + + for (var subkey in workflow.triggers[key].parameters) { + const param = workflow.triggers[key].parameters[subkey] + if (param.name === "url") { + webhook.url = param.value + + if (isLoading === false) { + setIsLoading(true) + } + break + } + } + } + } else if (data.type == "authenticate") { + //console.log("Handle app authentication in the workflow!") + } + + return ( +
{ + setIsOpen(!isOpen) + setActiveStep(index) + }} + onMouseOver={() => { + setHovered(true); + }} + onMouseOut={() => { + setHovered(false); + }} + > + +
+ {data.title} + {finished ? + + : + + } +
+ + {activeStep === index ? +
+ {data.type === "webhook" ? +
{ + event.preventDefault() + console.log("Clicked Webhook") + + var copyText = document.getElementById("copy_element_shuffle") + if (copyText !== undefined && copyText !== null) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(webhook.url); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + alert.success("Copied Webhook URL"); + } + }}> + {webhook.description} + {/*{webhook.url}*/} + {isLoading && finished === false ? +
+ +
+ : + null + } +
+ : + + } +
+ : null} + +
) } - const AppSection = (props) => { - const {action} = props + const AppWrapper = (props) => { + const {data, parentindex} = props + const [clicked, setClicked] = useState(true) + const [hovered, setHovered] = useState(false) + const [activeStep, setActiveStep] = useState(0) + const [firstRun, setFirstRun] = useState(true) + const [finishCount, setFinishCount] = useState(0) return ( - - - - {action.app_name} - - - - {action.must_authenticate ? - action.auth_done ? - - : - selectedAction.app_name === action.app_name ? - - : - - : - null} - {action.must_activate ? - - : null} - + if (index === activeStep && firstRun === true) { + setActiveStep(activeStep+1) + } + + if (firstRun) { + setFinishCount(finishCount+1) + } + } + } + + if (step.type === "authenticate") { + console.log("AUTH STEP: ", step) + if (data.must_authenticate === true ) { + finished = false + } else { + if (data.activation_done === true && data.auth_done === true) { + finished = true + + if (firstRun) { + setFinishCount(finishCount+1) + } + + if (index === activeStep && firstRun === true) { + setActiveStep(activeStep+1) + } + } + } + } + + if (step.type === "webhook") { + for (var key in workflowExecutions) { + const exec = workflowExecutions[key] + if (exec.execution_argument !== undefined && exec.execution_argument !== null && exec.execution_argument.length > 0 && exec.execution_source === "webhook") { + //console.log("Done: ", exec) + + finished = true + if (index === activeStep && firstRun === true) { + setActiveStep(activeStep+1) + + } + + if (firstRun) { + setFinishCount(finishCount+1) + } + + // Finished + source = webhook + + stop() + //if (isLoading === true) { + // setIsLoading(false) + //} + + + break + } + } + } + + if (firstRun === true && index === data.steps.length-1) { + setFirstRun(false) + } + + return ( + + ) + }) + : null} + ) } - const activateApp = (app_id, app_name, app_version) => { - fetch(`${globalUrl}/api/v1/apps/app_id/activate?app_name=${app_name}&app_version=${app_version}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - //window.location.pathname = "/search" - //alert.error("Failed to find this app. Is it public?") - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.success === false) { - alert.error("Failed to activate the app") - } else { - alert.success("App activated for your organization!") - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - return ( -
- {workflow.name} - - The following configuration makes the workflow ready immediately. - - {requiredActions.length > 0 ? - - Actions - - {requiredActions.map((data, index) => { - return ( - - ) - })} - - - : null} - - {requiredVariables.length > 0 ? - - Variables - - {requiredVariables.map((data, index) => { - return ( - - ) - })} - - - : null} - - - {requiredTriggers.length > 0 ? - - Triggers - - {requiredTriggers.map((data, index) => { - return ( - - ) - })} - - - : null } -
- - {/* - - */} - - + const topColor = "#f86a3e, #fc3922" + return ( +
+
-
- ) -} +
+ {workflow.name} + + The following configuration makes the workflow ready immediately. + + {requiredActions.length > 0 ? ( + + + Required Actions + -export default ConfigureWorkflow + + {requiredActions.map((data, index) => { + return ( +
+ {data.steps !== undefined && data.steps !== null && data.show_steps === true ? + + : + + } +
+ ) + })} +
+
+ ) : null} + + {requiredVariables.length > 0 ? ( + + + Variables + + + {requiredVariables.map((data, index) => { + return ; + })} + + + ) : null} + + {requiredTriggers.length > 0 && showTriggers !== false ? ( + + + Triggers + + + {requiredTriggers.map((data, index) => { + return ; + })} + + + ) : null} + +
+ {showFinalizeAnimation ? + finalize workflow animation { + console.log("Img loaded.") + setTimeout(() => { + console.log("Img closing.") + setConfigureWorkflowModalOpen(false); + }, 1250) + + }}/> + : + + {/* + + */} + + + } +
+
+
+ ); +}; + +export default ConfigureWorkflow; diff --git a/frontend/src/components/Dropzone.js b/frontend/src/components/Dropzone.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/FooterNew.js b/frontend/src/components/FooterNew.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/LoginPopup.js b/frontend/src/components/LoginPopup.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/NestedMenu.jsx b/frontend/src/components/NestedMenu.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx old mode 100644 new mode 100755 index 4fd89ac7..9832b990 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -1,216 +1,664 @@ -import React, {useRef, useState, useEffect, useLayoutEffect} from 'react'; -import { useTheme } from '@material-ui/core/styles'; +import React, { useRef, useState, useEffect, useLayoutEffect } from "react"; +import { useParams, useNavigate, Link } from "react-router-dom"; +import { useTheme } from "@material-ui/core/styles"; +import theme from '../theme.jsx'; +import { useAlert } from "react-alert"; -import { v4 as uuidv4 } from 'uuid'; -import { ListItemText, TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade } from '@material-ui/core'; -import { LockOpen as LockOpenIcon } from '@material-ui/icons'; +import { v4 as uuidv4 } from "uuid"; +import { + ListItemText, + TextField, + Drawer, + Button, + Paper, + Grid, + Tabs, + InputAdornment, + Tab, + ButtonBase, + Tooltip, + Select, + MenuItem, + Divider, + Dialog, + Modal, + DialogActions, + DialogTitle, + InputLabel, + DialogContent, + FormControl, + IconButton, + Menu, + Input, + FormGroup, + FormControlLabel, + Typography, + Checkbox, + Breadcrumbs, + CircularProgress, + Switch, + Fade, +} from "@material-ui/core"; +import { + LockOpen as LockOpenIcon, + SupervisorAccount as SupervisorAccountIcon, +} from "@mui/icons-material"; -const ITEM_HEIGHT = 55 -const ITEM_PADDING_TOP = 8 +const ITEM_HEIGHT = 55; +const ITEM_PADDING_TOP = 8; const MenuProps = { PaperProps: { style: { maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - minWidth: 500, + minWidth: 500, maxWidth: 500, - scrollX: "auto", + scrollX: "auto", }, }, -} + variant: "menu", + getContentAnchorEl: null, +}; + +const registeredApps = [ + "gmail", + "slack", + "webex", + "zoho_desk", + "outlook_graph", + "outlook_office365", + "microsoft_teams", + "microsoft_teams_user_access", + "todoist", + "microsoft_sentinel", + "microsoft_365_defender", + "google_sheets", + "google_drive", + "google_disk", + "jira", + "jira_service_desk", + "jira_service_management", + "github", +] const AuthenticationOauth2 = (props) => { - const { saveWorkflow, selectedApp, workflow, selectedAction, authenticationType, getAppAuthentication, appAuthentication, setSelectedAction, setNewAppAuth, setAuthenticationModalOpen} = props; - const theme = useTheme(); + const { + saveWorkflow, + selectedApp, + workflow, + selectedAction, + authenticationType, + getAppAuthentication, + appAuthentication, + setSelectedAction, + setNewAppAuth, + isCloud, + autoAuth, + authButtonOnly, + isLoggedIn, + } = props; - //const [update, setUpdate] = React.useState("|") - const [defaultConfigSet, setDefaultConfigSet] = React.useState(authenticationType.client_id !== undefined && authenticationType.client_id !== null && authenticationType.client_id.length > 0 && authenticationType.client_secret !== undefined && authenticationType.client_secret !== null && authenticationType.client_secret.length > 0) - const [clientId, setClientId] = React.useState(defaultConfigSet ? authenticationType.client_id : "") - const [clientSecret, setClientSecret] = React.useState(defaultConfigSet ? authenticationType.client_secret : "") - const [oauthUrl, setOauthUrl] = React.useState("") - const [buttonClicked, setButtonClicked] = React.useState(false) - const [selectedScopes, setSelectedScopes] = React.useState([]) - const allscopes = authenticationType.scope !== undefined ? authenticationType.scope: [] + let navigate = useNavigate(); + const alert = useAlert() - const [manuallyConfigure, setManuallyConfigure] = React.useState(defaultConfigSet ? false : true) - const [authenticationOption, setAuthenticationOptions] = React.useState({ - app: JSON.parse(JSON.stringify(selectedApp)), - fields: {}, - label: "", - usage: [{ - workflow_id: workflow.id, - }], - id: uuidv4(), - active: true, - }) + //const [update, setUpdate] = React.useState("|") + const [defaultConfigSet, setDefaultConfigSet] = React.useState( + authenticationType.client_id !== undefined && + authenticationType.client_id !== null && + authenticationType.client_id.length > 0 && + authenticationType.client_secret !== undefined && + authenticationType.client_secret !== null && + authenticationType.client_secret.length > 0 + ); - if (selectedApp.authentication === undefined) { - return null - } + const [clientId, setClientId] = React.useState( + defaultConfigSet ? authenticationType.client_id : "" + ); + const [clientSecret, setClientSecret] = React.useState( + defaultConfigSet ? authenticationType.client_secret : "" + ); + const [oauthUrl, setOauthUrl] = React.useState(""); + const [buttonClicked, setButtonClicked] = React.useState(false); - const handleOauth2Request = (client_id, client_secret, oauth_url, scopes) => { - setButtonClicked(true) - console.log("SCOPES: ", scopes) + const [offlineAccess, setOfflineAccess] = React.useState(true); + const allscopes = authenticationType.scope !== undefined ? authenticationType.scope : []; + - var resources = "" - if (scopes !== undefined && scopes !== null & scopes.length > 0) { - resources = scopes.join(",") + const [selectedScopes, setSelectedScopes] = React.useState(allscopes.length === 1 ? [allscopes[0]] : []) + const [manuallyConfigure, setManuallyConfigure] = React.useState( + defaultConfigSet ? false : true + ); + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + workflow_id: workflow !== undefined ? workflow.id : "", + }, + ], + id: uuidv4(), + active: true, + }); + + + useEffect(() => { + if (isLoggedIn === false) { + navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`) } - const authentication_url = authenticationType.token_uri - - console.log("SCOPES2: ", resources) - const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication` - var state = `workflow_id%3D${workflow.id}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}` - if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) { - state += `%26oauth_url%3d${oauth_url}` - console.log("ADDING OAUTH2 URL: ", state) + console.log("Should automatically click the auto-auth button?: ", autoAuth) + if (autoAuth === true && selectedApp !== undefined) { + startOauth2Request() } - const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}` + }, []) - //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` - console.log("Full URI: ", url) - console.log("Redirect Uri: ", redirectUri) - // &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 { + if (selectedApp.authentication === undefined) { + return null; + } - var newwin = window.open(url, "", "width=800,height=600") - //console.log(newwin) - - var open = true - const timer = setInterval(() => { - if (newwin.closed) { - setButtonClicked(false) - clearInterval(timer); - //alert('"Secure Payment" window closed!'); + const startOauth2Request = (admin_consent) => { + // Admin consent also means to add refresh tokens + console.log("Inside oauth2 request for app: ", selectedApp.name) + selectedApp.name = selectedApp.name.replace(" ", "_").toLowerCase() - getAppAuthentication(true, true) - } - }, 1000); - //do { - // setTimeout(() => { - // console.log(newwin) - // console.log("CLOSED", newwin.closed) - // if (newwin.closed) { - - // open = false - // } - // }, 1000) - //} - //while(open === true) - } catch (e) { - alert.error("Failed authentication - probably bad credentials. Try again") - setButtonClicked(false) - } - - return - //do { - //} while ( - } - - - authenticationOption.app.actions = [] - - for (var key in selectedApp.authentication.parameters) { - if (authenticationOption.fields[selectedApp.authentication.parameters[key].name] === undefined) { - authenticationOption.fields[selectedApp.authentication.parameters[key].name] = "" - } - } - - const handleSubmitCheck = () => { - console.log("NEW AUTH: ", authenticationOption) - if (authenticationOption.label.length === 0) { - authenticationOption.label = `Auth for ${selectedApp.name}` - //alert.info("Label can't be empty") - //return - } - - // Automatically mapping fields that already exist (predefined). - // Warning if fields are NOT filled - for (var key in selectedApp.authentication.parameters) { - if (authenticationOption.fields[selectedApp.authentication.parameters[key].name].length === 0) { - if (selectedApp.authentication.parameters[key].value !== undefined && selectedApp.authentication.parameters[key].value !== null && selectedApp.authentication.parameters[key].value.length > 0) { - authenticationOption.fields[selectedApp.authentication.parameters[key].name] = selectedApp.authentication.parameters[key].value - } else { - if (selectedApp.authentication.parameters[key].schema.type === "bool") { - authenticationOption.fields[selectedApp.authentication.parameters[key].name] = "false" - } else { - alert.info("Field "+selectedApp.authentication.parameters[key].name+" can't be empty") - return - } - } - } - } - - console.log("Action: ", selectedAction) - selectedAction.authentication_id = authenticationOption.id - selectedAction.selectedAuthentication = authenticationOption - if (selectedAction.authentication === undefined || selectedAction.authentication === null) { - selectedAction.authentication = [authenticationOption] + //console.log("APP: ", selectedApp) + if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") { + handleOauth2Request( + "efe4c3fe-84a1-4821-a84f-23a6cfe8e72d", + "", + "https://graph.microsoft.com", + ["Mail.ReadWrite", "Mail.Send", "offline_access"], + admin_consent, + ); + } else if (selectedApp.name.toLowerCase() == "gmail") { + handleOauth2Request( + "253565968129-6ke8086pkp0at16m8t95rdcsas69ngt1.apps.googleusercontent.com", + "", + "https://gmail.googleapis.com", + ["https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.send", + "https://www.googleapis.com/auth/gmail.insert", + "https://www.googleapis.com/auth/gmail.compose", + ], + admin_consent, + "select_account%20consent", + ) + } else if (selectedApp.name.toLowerCase() == "zoho_desk") { + handleOauth2Request( + "1000.ZR5MHUW6B0L6W1VUENFGIATFS0TOJT", + "", + "https://desk.zoho.com", + ["Desk.tickets.READ", + "Desk.tickets.UPDATE", + "Desk.tickets.DELETE", + "Desk.tickets.CREATE", + "offline_access"], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase() == "slack") { + handleOauth2Request( + "5155508477298.5168162485601", + "", + "https://slack.com", + ["chat:write:user", "im:read", "im:write", "search:read", "usergroups:read", "usergroups:write",], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase() == "webex") { + handleOauth2Request( + "Cab184f3d7271f540443c79b5b79845e3387abbbdb3db4233a87ea3a5432fb3d5", + "", + "https://webexapis.com", + ["spark:all"], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("microsoft_teams")) { + handleOauth2Request( + "31cb4c84-658e-43d5-ae84-22c9142e967a", + "", + "https://graph.microsoft.com", + ["ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "Chat.Create", "Chat.ReadWrite", "Chat.Read", "offline_access"], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("todoist")) { + handleOauth2Request( + "35fa3a384040470db0c8527e90a3c2eb", + "", + "https://api.todoist.com", + ["task:add",], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("microsoft_sentinel")) { + handleOauth2Request( + "4c16e8c4-3d34-4aa1-ac94-262ea170b7f7", + "", + "https://management.azure.com", + ["https://management.azure.com/user_impersonation",], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("microsoft_365_defender")) { + handleOauth2Request( + "4c16e8c4-3d34-4aa1-ac94-262ea170b7f7", + "", + "https://graph.microsoft.com", + ["SecurityEvents.ReadWrite.All",], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("google_sheets")) { + handleOauth2Request( + "253565968129-mppu17aciek8slr3kpgnb37hp86dmvmb.apps.googleusercontent.com", + "", + "https://sheets.googleapis.com", + ["https://www.googleapis.com/auth/spreadsheets"], + admin_consent, + "consent", + ) + } else if (selectedApp.name.toLowerCase().includes("google_drive") || selectedApp.name.toLowerCase().includes("google_disk")) { + handleOauth2Request( + "253565968129-6pij4g6ojim4gpum0h9m9u3bc357qsq7.apps.googleusercontent.com", + "", + "https://www.googleapis.com", + ["https://www.googleapis.com/auth/drive",], + admin_consent, + "consent", + ) + } else if (selectedApp.name.toLowerCase().includes("jira_service_desk") || selectedApp.name.toLowerCase().includes("jira") || selectedApp.name.toLowerCase().includes("jira_service_management")) { + handleOauth2Request( + "AI02egeCQh1Zskm1QAJaaR6dzjR97V2F", + "", + "https://api.atlassian.com", + ["read:jira-work", "write:jira-work", "read:servicedesk:jira-service-management", "write:servicedesk:jira-service-management", "read:request:jira-service-management", "write:request:jira-service-management",], + admin_consent, + ) + } else if (selectedApp.name.toLowerCase().includes("github")) { + handleOauth2Request( + "3d272b1b782b100b1e61", + "", + "https://api.github.com", + ["repo","user","project","notifications",], + admin_consent, + ) } else { - selectedAction.authentication.push(authenticationOption) + console.log("No match found for: ", selectedApp.name) + } + // write:request:jira-service-management + } + + + const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent, prompt) => { + setButtonClicked(true); + //console.log("SCOPES: ", scopes); + + client_id = client_id.trim() + client_secret = client_secret.trim() + oauth_url = oauth_url.trim() + + var resources = ""; + if (scopes !== undefined && (scopes !== null) & (scopes.length > 0)) { + console.log("IN scope 1") + if (offlineAccess === true && !scopes.includes("offline_access")) { + + console.log("IN scope 2") + if (!authenticationType.redirect_uri.includes("google")) { + console.log("Appending offline access") + scopes.push("offline_access") + } + } + + resources = scopes.join(" "); + //resources = scopes.join(","); + } + + const authentication_url = authenticationType.token_uri; + //console.log("AUTH: ", authenticationType) + //console.log("SCOPES2: ", resources) + const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`; + const workflowId = workflow !== undefined ? workflow.id : ""; + var state = `workflow_id%3D${workflowId}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}`; + + + // This is to make sure authorization can be handled WITHOUT being logged in, + // kind of making it act like an api key + // https://shuffler.io/authorization -> 3rd party integration auth + const urlParams = new URLSearchParams(window.location.search); + const userAuth = urlParams.get("authorization"); + if (userAuth !== undefined && userAuth !== null && userAuth.length > 0) { + console.log("Adding authorization from user side") + state += `%26authorization%3d${userAuth}`; } - setSelectedAction(selectedAction) - - var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)) - var newFields = [] - for (const key in newAuthOption.fields) { - const value = newAuthOption.fields[key] - newFields.push({ - key: key, - value: value, - }) + // Check for org_id + const orgId = urlParams.get("org_id"); + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + console.log("Adding org_id from user side") + state += `%26org_id%3d${orgId}`; } - console.log("FIELDS: ", newFields) - newAuthOption.fields = newFields - setNewAppAuth(newAuthOption) - //appAuthentication.push(newAuthOption) - //setAppAuthentication(appAuthentication) - // + if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) { + state += `%26oauth_url%3d${oauth_url}`; + console.log("ADDING OAUTH2 URL: ", state); + } + + + if ( + authenticationType.refresh_uri !== undefined && + authenticationType.refresh_uri !== null && + authenticationType.refresh_uri.length > 0 + ) { + state += `%26refresh_uri%3d${authenticationType.refresh_uri}`; + } else { + 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`; + var defaultPrompt = "login" + if (prompt !== undefined && prompt !== null && prompt.length > 0) { + defaultPrompt = prompt + } - //if (configureWorkflowModalOpen) { - // setSelectedAction({}) - //} - //setUpdate(authenticationOption.id) + 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`; + } + + console.log("URL: ", url) + + // 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 + //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) + + var open = true; + const timer = setInterval(() => { + if (newwin.closed) { + console.log("Closing?") + + + setButtonClicked(false); + clearInterval(timer); + //alert('"Secure Payment" window closed!'); + // + + if (getAppAuthentication !== undefined) { + getAppAuthentication(true, true, true); + } + } else { + console.log("Not closed") + } + }, 1000); + //do { + // setTimeout(() => { + // console.log(newwin) + // console.log("CLOSED", newwin.closed) + // if (newwin.closed) { + + // open = false + // } + // }, 1000) + //} + //while(open === true) + } catch (e) { + alert.error( + "Failed authentication - probably bad credentials. Try again" + ); + setButtonClicked(false); + } + + return; + //do { + //} while ( + }; + + authenticationOption.app.actions = []; + + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = ""; + } + } + + const handleSubmitCheck = () => { + console.log("NEW AUTH: ", authenticationOption); + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + //alert.info("Label can't be empty") + //return + } + + // Automatically mapping fields that already exist (predefined). + // Warning if fields are NOT filled + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[key].value !== undefined && + selectedApp.authentication.parameters[key].value !== null && + selectedApp.authentication.parameters[key].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = selectedApp.authentication.parameters[key].value; + } else { + if ( + selectedApp.authentication.parameters[key].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = "false"; + } else { + alert.info( + "Field " + selectedApp.authentication.parameters[key].name.replace("_basic", "", -1).replace("_", " ", -1) + " can't be empty" + + ); + return; + } + } + } + } + + console.log("Action: ", selectedAction); + selectedAction.authentication_id = authenticationOption.id; + selectedAction.selectedAuthentication = authenticationOption; + if ( + selectedAction.authentication === undefined || + selectedAction.authentication === null + ) { + selectedAction.authentication = [authenticationOption]; + } else { + selectedAction.authentication.push(authenticationOption); + } + + setSelectedAction(selectedAction); + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (const key in newAuthOption.fields) { + const value = newAuthOption.fields[key]; + newFields.push({ + key: key, + value: value, + }); + } + + console.log("FIELDS: ", newFields); + newAuthOption.fields = newFields; + setNewAppAuth(newAuthOption); + //appAuthentication.push(newAuthOption) + //setAppAuthentication(appAuthentication) + // + + //if (configureWorkflowModalOpen) { + // setSelectedAction({}) + //} + //setUpdate(authenticationOption.id) + + /* {selectedAction.authentication.map(data => ( */ - - } + }; const handleScopeChange = (event) => { const { target: { value }, - } = event; + } = event; - console.log("VALUE: ", value) + console.log("VALUE: ", value); // On autofill we get a the stringified value. - setSelectedScopes(typeof value === 'string' ? value.split(',') : value) + setSelectedScopes(typeof value === "string" ? value.split(",") : value); + }; + + if ( + authenticationOption.label === null || + authenticationOption.label === undefined + ) { + authenticationOption.label = selectedApp.name + " authentication"; } - if (authenticationOption.label === null || authenticationOption.label === undefined) { - authenticationOption.label = selectedApp.name+" authentication" + const autoAuthButton = + + + if (authButtonOnly === true) { + return autoAuthButton } - //console.log( - return ( -
-
Authentication for {selectedApp.name}
- - - Oauth2 requires a client ID and secret to authenticate. This is usually made in the remote system. - Learn more about Oauth2 with Shuffle
- - {/* + +
+ Authentication for {selectedApp.name} +
+
+ + + Oauth2 requires a client ID and secret to authenticate, defined in the remote system. Your redirect URL is {window.location.origin}/set_authentication -  + + {" "} + Learn more about Oauth2 with Shuffle + +
+ + + {isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ? + + + {autoAuthButton} + + {buttonClicked ? + null + : + + + + } + + + OR + + + : null} + {/* { */} - {!manuallyConfigure ? null : - - {selectedApp.authentication.parameters.map((data, index) => { - //console.log(data, index) - if (data.name === "client_id" || data.name === "client_secret") { - return null - } + {!manuallyConfigure ? null : ( + + {selectedApp.authentication.parameters.map((data, index) => { + //console.log(data, index) + if (data.name === "client_id" || data.name === "client_secret") { + return null; + } - if (data.name !== "url") { - return null - } + if (data.name !== "url") { + return null; + } - if (oauthUrl.length === 0) { - setOauthUrl(data.value) - } + if (oauthUrl.length === 0) { + setOauthUrl(data.value); + } - return ( -
- - {data.name} + return ( +
+ + {data.name} - {data.schema !== undefined && data.schema !== null && data.schema.type === "bool" ? - - : - { - authenticationOption.fields[data.name] = event.target.value - console.log("Setting oauth url") - setOauthUrl(event.target.value) - //const [oauthUrl, setOauthUrl] = React.useState("") - }} - /> - } -
- ) - })} - {allscopes.length === 0 ? null : - } - renderValue={(selected) => selected.join(', ')} - MenuProps={MenuProps} - > - {allscopes.map((data, index) => { - return ( - - -1} /> - - - ) - })} - - } - { - setClientId(event.target.value) - //authenticationOption.label = event.target.value - }} - /> - { - setClientSecret(event.target.value) - //authenticationOption.label = event.target.value - }} - /> - - } - + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + console.log("Setting oauth url"); + setOauthUrl(event.target.value); + //const [oauthUrl, setOauthUrl] = React.useState("") + }} + /> + )} +
+ ); + })} + { + setClientId(event.target.value); + //authenticationOption.label = event.target.value + }} + /> + { + setClientSecret(event.target.value); + //authenticationOption.label = event.target.value + }} + /> + {allscopes.length === 0 ? null : ( +
+ + Scopes + } + renderValue={(selected) => selected.join(", ")} + MenuProps={MenuProps} + > + {allscopes.map((data, index) => { + return ( + + -1} /> + + + ); + })} + + + + + { + setOfflineAccess(!offlineAccess) + }}/> + + +
+ )} +
+ )} + - {defaultConfigSet ? - - ... or - - - : - null - } - -
- ) -} -export default AuthenticationOauth2 + {defaultConfigSet ? ( + + ... or + + + ) : null} +
+
+ ); +}; + +export default AuthenticationOauth2; diff --git a/frontend/src/components/OrgHeader.js b/frontend/src/components/OrgHeader.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx old mode 100644 new mode 100755 index ef635b6f..1871dee7 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -1,352 +1,831 @@ -import React, {useState, useEffect, useLayoutEffect} from 'react'; -import { makeStyles, createStyles } from '@material-ui/core/styles'; +import React, { useState, useEffect, useLayoutEffect } from "react"; +import { makeStyles, createStyles } from "@material-ui/core/styles"; +import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import { GetParsedPaths } from "../views/Apps.jsx"; -import { GetIconInfo } from "../views/Workflows.jsx"; import { sortByKey } from "../views/AngularWorkflow.jsx"; -import { useTheme } from '@material-ui/core/styles'; +import { useTheme } from "@material-ui/core/styles"; import NestedMenuItem from "material-ui-nested-menu-item"; -//import NestedMenuItem from "./NestedMenu.jsx"; +import { useAlert } from "react-alert"; +import theme from '../theme.jsx'; -import {Popper, TextField, TextareaAutosize, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade} from '@material-ui/core'; -import {HelpOutline as HelpOutlineIcon, Description as DescriptionIcon, GetApp as GetAppIcon, Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import { + ButtonGroup, + Popper, + TextField, + TextareaAutosize, + Drawer, + Button, + Paper, + Tabs, + InputAdornment, + Tab, + ButtonBase, + Tooltip, + Select, + MenuItem, + Divider, + Dialog, + Modal, + DialogActions, + DialogTitle, + InputLabel, + DialogContent, + FormControl, + IconButton, + Menu, + Input, + FormGroup, + FormControlLabel, + Typography, + Checkbox, + Breadcrumbs, + CircularProgress, + Switch, + Fade, +} from "@material-ui/core"; -import CodeMirror from '@uiw/react-codemirror'; -import 'codemirror/keymap/sublime'; -import 'codemirror/theme/gruvbox-dark.css'; +import { + Autocomplete +} from "@material-ui/lab"; +import { + HelpOutline as HelpOutlineIcon, + Description as DescriptionIcon, + GetApp as GetAppIcon, + Search as SearchIcon, + ArrowUpward as ArrowUpwardIcon, + Visibility as VisibilityIcon, + Done as DoneIcon, + Close as CloseIcon, + Error as ErrorIcon, + FindReplace as FindreplaceIcon, + ArrowLeft as ArrowLeftIcon, + Cached as CachedIcon, + DirectionsRun as DirectionsRunIcon, + Add as AddIcon, + Polymer as PolymerIcon, + FormatListNumbered as FormatListNumberedIcon, + Create as CreateIcon, + PlayArrow as PlayArrowIcon, + AspectRatio as AspectRatioIcon, + MoreVert as MoreVertIcon, + Apps as AppsIcon, + Schedule as ScheduleIcon, + FavoriteBorder as FavoriteBorderIcon, + Pause as PauseIcon, + Delete as DeleteIcon, + AddCircleOutline as AddCircleOutlineIcon, + Save as SaveIcon, + KeyboardArrowLeft as KeyboardArrowLeftIcon, + KeyboardArrowRight as KeyboardArrowRightIcon, + ArrowBack as ArrowBackIcon, + Settings as SettingsIcon, + LockOpen as LockOpenIcon, + ExpandMore as ExpandMoreIcon, + VpnKey as VpnKeyIcon, + AutoFixHigh as AutoFixHighIcon, + Circle as CircleIcon, + SquareFoot as SquareFootIcon, +} from '@mui/icons-material'; +//} from "@material-ui/icons"; + + +//import CodeMirror from "@uiw/react-codemirror"; +//import "codemirror/keymap/sublime"; +//import "codemirror/theme/gruvbox-dark.css"; +import ShuffleCodeEditor from "../components/ShuffleCodeEditor.jsx"; const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important" - }, - root: { - "& .MuiAutocomplete-listbox": { - border: "2px solid grey", - color: "white", - fontSize: 18, - "& li:nth-child(even)": { - backgroundColor: "#CCC" - }, - "& li:nth-child(odd)": { - backgroundColor: "#FFF" - } - } - }, - inputRoot: { - color: "white", - // This matches the specificity of the default styles at https://github.com/mui-org/material-ui/blob/v4.11.3/packages/material-ui-lab/src/Autocomplete/Autocomplete.js#L90 - "&:hover .MuiOutlinedInput-notchedOutline": { - borderColor: "#f86a3e" - }, - } -}) -//const useStyles = makeStyles((theme) => -// createStyles({ -// notchedOutline: { -// borderColor: "#f85a3e !important" -// }, -//) - + notchedOutline: { + borderColor: "#f85a3e !important", + }, + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid grey", + color: "white", + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: "white", + // This matches the specificity of the default styles at https://github.com/mui-org/material-ui/blob/v4.11.3/packages/material-ui-lab/src/Autocomplete/Autocomplete.js#L90 + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, +}); + +const openApiFieldDesc = "Generated by OpenAPI body example"; const ParsedAction = (props) => { - const {workflow, setWorkflow, setAction, setSelectedAction, setUpdate, appActionArguments, selectedApp, workflowExecutions, setSelectedResult, selectedAction, setSelectedApp, setSelectedTrigger, setSelectedEdge, setCurrentView, cy, setAuthenticationModalOpen,setVariablesModalOpen, setCodeModalOpen, selectedNameChange, rightsidebarStyle, showEnvironment, selectedActionEnvironment, environments, setNewSelectedAction, appApiViewStyle, globalUrl, setSelectedActionEnvironment, requiresAuthentication, hideExtraTypes, scrollConfig, setScrollConfig, authenticationType, appAuthentication, getAppAuthentication } = props + const { + workflow, + files, + setWorkflow, + setAction, + setSelectedAction, + setUpdate, + appActionArguments, + selectedApp, + workflowExecutions, + setSelectedResult, + selectedAction, + setSelectedApp, + setSelectedTrigger, + setSelectedEdge, + setCurrentView, + cy, + setAuthenticationModalOpen, + setVariablesModalOpen, + setCodeModalOpen, + selectedNameChange, + rightsidebarStyle, + showEnvironment, + selectedActionEnvironment, + environments, + setNewSelectedAction, + appApiViewStyle, + globalUrl, + setSelectedActionEnvironment, + requiresAuthentication, + hideExtraTypes, + scrollConfig, + setScrollConfig, + authenticationType, + appAuthentication, + getAppAuthentication, + actionDelayChange, + getParents, + isCloud, + lastSaved, + setLastSaved, + setShowVideo, + toolsAppId, + aiSubmit, + //expansionModalOpen, + //setExpansionModalOpen, + } = props; - const theme = useTheme(); - const classes = useStyles() + //const theme = useTheme(); + const classes = useStyles(); + const alert = useAlert() - const [expansionModalOpen, setExpansionModalOpen] = React.useState(false); + const [expansionModalOpen, setExpansionModalOpen] = React.useState(false); + const [hideBody, setHideBody] = React.useState(true); + const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false); - const keywords = ["len(", "lower(", "upper(", "trim(", "split(", "length(", "number(", "parse(", "join("] - const getParents = (action) => { - if (cy === undefined) { - return [] + const [codedata, setcodedata] = React.useState(""); + const [fieldCount, setFieldCount] = React.useState(0); + const [hiddenDescription, setHiddenDescription] = React.useState(true); + + + useEffect(() => { + if (setLastSaved !== undefined) { + setLastSaved(false) } + }, [expansionModalOpen]) - var allkeys = [action.id] - var handled = [] - var results = [] - - while(true) { - for (var key in allkeys) { - var currentnode = cy.getElementById(allkeys[key]) - if (currentnode === undefined) { - continue - } - - if (handled.includes(currentnode.data("id"))) { - continue + useEffect(() => { + if (selectedAction.parameters !== null && selectedAction.parameters !== undefined) { + const paramcheck = selectedAction.parameters.find(param => param.name === "body") + //console.log("LOADED! Change hideBody based on input? Action: ", selectedAction, paramcheck) + if (paramcheck !== undefined && paramcheck !== null) { + if (paramcheck.id === "TOGGLED"){ + setHideBody(false) + setActivateHidingBodyButton(false) } else { - // Get the name / label here too? - handled.push(currentnode.data("id")) - results.push(currentnode.data()) - } + setHideBody(true) - if (currentnode.length === 0) { - continue - } - - const incomingEdges = currentnode.incomers('edge') - if (incomingEdges.length === 0) { - continue - } - - for (var i = 0; i < incomingEdges.length; i++) { - var tmp = incomingEdges[i] - if (!allkeys.includes(tmp.data().source)) { - allkeys.push(tmp.data().source) + if (paramcheck.id === "UNTOGGLED") { + setActivateHidingBodyButton(false) } } } - if (results.length === allkeys.length) { - break - } } + }, []) - // Some obscure bug made this have to be done.. zz - results = results.filter(data => data !== undefined && action !== undefined && data.id !== action.id) - results = results.filter(data => data !== undefined && data.type !== "TRIGGER") - results.push({"label": "Execution Argument", "type": "INTERNAL"}) - return results - } + const keywords = [ + "len(", + "lower(", + "upper(", + "trim(", + "split(", + "length(", + "number(", + "parse(", + "join(", + ]; + + const getApp = (appId, setApp) => { + fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //alert.success("Successfully GOT app "+appId) + } else { + alert.error("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESPONSE: ", responseJson); + + const parsedapp = + responseJson.app !== undefined && responseJson.app !== null + ? JSON.parse(atob(responseJson.app)) + : {}; + console.log("PARSED: ", parsedapp); + //data = parsedapp.body === undefined ? parsedapp : parsedapp.body + + if ( + setApp && + parsedapp.actions !== undefined && + parsedapp.actions !== null + ) { + console.log("Inside first if"); + if ( + selectedApp.versions !== undefined && + selectedApp.versions !== null + ) { + parsedapp.versions = selectedApp.versions; + } + + if ( + selectedApp.loop_versions !== undefined && + selectedApp.loop_versions !== null + ) { + parsedapp.loop_versions = selectedApp.loop_versions; + } + + // Find authentication, and if it works? + // If authentication has less OR more fields, it has to change + //console.log(selected + + console.log("Inside first if2"); + var foundAction = parsedapp.actions.find( + (action) => + action.name.toLowerCase() === selectedAction.name.toLowerCase() + ); + console.log("FOUNDACTION: ", foundAction); + if (foundAction !== null && foundAction !== undefined) { + var foundparams = []; + for (let [paramkey,paramkeyval] in Object.entries(foundAction.parameters)) { + const param = foundAction.parameters[paramkey]; + + const foundParam = selectedAction.parameters.find( + (item) => item.name.toLowerCase() === param.name.toLowerCase() + ); + if (foundParam === undefined) { + console.log("COULDNT find Param: ", param); + } else { + foundAction.parameters[paramkey] = foundParam; + } + //foundparams.push(param.name) + } + } else { + alert.error("Couldn't find action " + selectedAction.name); + } + + selectedAction.errors = []; + selectedAction.is_valid = true; + + // Updating params for the new action + selectedAction.parameters = foundAction.parameters; + selectedAction.app_id = appId; + selectedAction.app_version = parsedapp.app_version; + + setSelectedAction(selectedAction); + setSelectedApp(parsedapp); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - const getApp = (appId, setApp) => { - fetch(globalUrl+"/api/v1/apps/"+appId+"/config?openapi=false", { - headers: { - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - //alert.success("Successfully GOT app "+appId) - } else { - alert.error("Failed getting app") - } - return response.json() - }) - .then((responseJson) => { - console.log("RESPONSE: ", responseJson) + const defineStartnode = () => { + if (cy === undefined) { + return; + } - const parsedapp = responseJson.app !== undefined && responseJson.app !== null ? JSON.parse(atob(responseJson.app)) : {} - console.log("PARSED: ", parsedapp) - //data = parsedapp.body === undefined ? parsedapp : parsedapp.body + var oldstartnode = cy.getElementById(workflow.start); + if (oldstartnode.length > 0) { + oldstartnode[0].data("isStartNode", false); + var oldnodecnt = workflow.actions.findIndex( + (a) => a.id === workflow.start + ); + if (workflow.actions[oldnodecnt] !== undefined) { + workflow.actions[oldnodecnt].isStartNode = false; + } + } - if (setApp && parsedapp.actions !== undefined && parsedapp.actions !== null) { - console.log("Inside first if") - if (selectedApp.versions !== undefined && selectedApp.versions !== null) { - parsedapp.versions = selectedApp.versions - } + var newstartnode = cy.getElementById(selectedAction.id); + if (newstartnode.length > 0) { + newstartnode[0].data("isStartNode", true); + var newnodecnt = workflow.actions.findIndex( + (a) => a.id === selectedAction.id + ); + console.log("NEW NODE CNT: ", newnodecnt); + if (workflow.actions[newnodecnt] !== undefined) { + workflow.actions[newnodecnt].isStartNode = true; + console.log(workflow.actions[newnodecnt]); + } + } - if (selectedApp.loop_versions !== undefined && selectedApp.loop_versions !== null) { - parsedapp.loop_versions = selectedApp.loop_versions - } + // Find branches with triggers as source nodes + // Move these targets to be the new node + // Set arrows pointing to new startnode with errors + //for (var key in workflow.branches) { + // var item = workflow.branches[key] + // if (item.destination_id === oldstartnode[0].data()["id"]) { + // var curbranch = cy.getElementById(item.id) + // if (curbranch.length > 0) { + // //console.log(curbranch[0].data()) + // //curbranch[0].data("target", selectedAction.id) + // //curbranch[0].data("hasErrors", true) + // //workflow.branches[key].destination_id = selectedAction.id + // //console.log(curbranch[0].data()) + // } + // } + //} - // Find authentication, and if it works? - // If authentication has less OR more fields, it has to change - //console.log(selected + setUpdate("start_node" + selectedAction.id); + workflow.start = selectedAction.id; + setWorkflow(workflow); + //setStartNode(selectedAction.id) + }; - console.log("Inside first if2") - var foundAction = parsedapp.actions.find(action => action.name.toLowerCase() === selectedAction.name.toLowerCase()) - console.log("FOUNDACTION: ", foundAction) - if (foundAction !== null && foundAction !== undefined) { - var foundparams = [] - for (var paramkey in foundAction.parameters) { - const param = foundAction.parameters[paramkey] + 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); - const foundParam = selectedAction.parameters.find(item => item.name.toLowerCase() === param.name.toLowerCase()) - if (foundParam === undefined) { - console.log("COULDNT find Param: ", param) - } else { - foundAction.parameters[paramkey] = foundParam - } - //foundparams.push(param.name) - } - } else { - alert.error("Couldn't find action "+selectedAction.name) - } + useEffect(() => { + if ( + selectedActionParameters !== null && + selectedActionParameters.length === 0 + ) { + if ( + 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); + } - selectedAction.errors = [] - selectedAction.is_valid = true - - // Updating params for the new action - selectedAction.parameters = foundAction.parameters - selectedAction.app_id = appId - selectedAction.app_version = parsedapp.app_version - - setSelectedAction(selectedAction) - setSelectedApp(parsedapp) - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const defineStartnode = () => { - if (cy === undefined) { - return - } - - var oldstartnode = cy.getElementById(workflow.start) - if (oldstartnode.length > 0) { - oldstartnode[0].data("isStartNode", false) - var oldnodecnt = workflow.actions.findIndex(a => a.id === workflow.start) - if (workflow.actions[oldnodecnt] !== undefined) { - workflow.actions[oldnodecnt].isStartNode = false - } - } - - var newstartnode = cy.getElementById(selectedAction.id) - if (newstartnode.length > 0) { - newstartnode[0].data("isStartNode", true) - var newnodecnt = workflow.actions.findIndex(a => a.id === selectedAction.id) - console.log("NEW NODE CNT: ", newnodecnt) - if (workflow.actions[newnodecnt] !== undefined) { - workflow.actions[newnodecnt].isStartNode = true - console.log(workflow.actions[newnodecnt]) - } - } - - // Find branches with triggers as source nodes - // Move these targets to be the new node - // Set arrows pointing to new startnode with errors - //for (var key in workflow.branches) { - // var item = workflow.branches[key] - // if (item.destination_id === oldstartnode[0].data()["id"]) { - // var curbranch = cy.getElementById(item.id) - // if (curbranch.length > 0) { - // //console.log(curbranch[0].data()) - // //curbranch[0].data("target", selectedAction.id) - // //curbranch[0].data("hasErrors", true) - // //workflow.branches[key].destination_id = selectedAction.id - // //console.log(curbranch[0].data()) - // } - // } - //} - - setUpdate("start_node"+selectedAction.id) - workflow.start = selectedAction.id - setWorkflow(workflow) - //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 !== null && selectedActionParameters.length === 0) { - if (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 - actionlist.push({"type": "Execution Argument", "name": "Execution Argument", "value": "$exec", "highlight": "exec", "autocomplete": "exec", "example": "hello"}) - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - for (var key in 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}) - } - } - - // FIXME: Add values from previous executions if they exist - if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) { - for (var key in workflow.execution_variables) { - const item = workflow.execution_variables[key] - actionlist.push({"type": "execution_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": ""}) - } - } - - // Loops parent nodes' old results to fix autocomplete - var parents = getParents(selectedAction) - if (parents.length > 1) { - for (var key in parents) { - const item = parents[key] - if (item.label === "Execution Argument") { - continue + 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; } - var exampledata = item.example === undefined ? "" : item.example - // Find previous execution and their variables - //exampledata === "" && - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false - for (var key in workflowExecutions) { - if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { - 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 + } + } - var foundResult = workflowExecutions[key].results.find(result => result.action.id === item.id) - if (foundResult === undefined) { - continue - } + } - foundResult.result = foundResult.result.trim() - foundResult.result = foundResult.result.split(" None").join(" \"None\"") - foundResult.result = foundResult.result.split(" False").join(" false") - foundResult.result = foundResult.result.split(" True").join(" true") + if (actionlist.length === 0) { + actionlist.push({ + type: "Execution Argument", + name: "Execution Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "", + }) + } - var jsonvalid = true - try { - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false + actionlist.push({ + type: "Shuffle DB", + name: "Shuffle DB", + value: "$shuffle_cache", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", + example: "", + }) + + 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, + }); + } + } + + // FIXME: Add values from previous executions if they exist + 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]; + actionlist.push({ + type: "execution_variable", + name: item.name, + value: item.value, + id: item.id, + autocomplete: `${item.name.split(" ").join("_")}`, + example: "", + }); + } + } + + // Loops parent nodes' old results to fix autocomplete + if (getParents !== undefined) { + var parents = getParents(selectedAction); + + 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 } - } catch (e) { - try { - foundResult.result = foundResult.result.split("\'").join("\"") - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false + + const valid = validateJson(foundResult) + if (valid.valid) { + if (valid.result.success === false) { + //console.log("Skipping success false autocomplete") + } else { + exampledata = valid.result; + break; } - } catch (e) { - jsonvalid = false + } else { + exampledata = foundResult; } - } + } + } - // Finds the FIRST json only - if (jsonvalid) { - exampledata = JSON.parse(foundResult.result) - break - } - //else { - // console.log("Invalid JSON: ", foundResult.result) - //} + // 1. Take + const itemlabelComplete = + item.label === null || item.label === undefined + ? "" + : item.label.split(" ").join("_"); + + const actionvalue = { + type: "action", + id: item.id, + name: item.label, + autocomplete: itemlabelComplete, + example: exampledata, + }; + + actionlist.push(actionvalue); + } + } + + //console.log("ACTIONLIST: ", actionlist) + setActionlist(actionlist); + } + } + }); + + + const calculateHelpertext = (input_data) => { + var helperText = "" + var looperText = "" + //const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) + var found = input_data.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) + + if (found !== null && found !== undefined) { + var new_occurences = [] + for (let [key,keyval] in Object.entries(found)) { + if (found[key][0] !== "\\") { + new_occurences.push(found[key]) + } + } + + found = new_occurences.valueOf() + } + + if (found !== null) { + try { + // When the found array is empty. + for (let i = 0; i < found.length; i++) { + const variableSplit = found[i].split(".#") + if ((variableSplit.length-1) > 1) { + //console.log("Larger than 1: ", variableSplit) + if (looperText.length === 0) { + looperText += "PS: Double looping (.#.#) may cause problems." } } - // 1. Take - const itemlabelComplete = item.label === null || item.label === undefined ? "" : item.label.split(" ").join("_") - const actionvalue = {"type": "action", "id": item.id, "name": item.label, "autocomplete": itemlabelComplete, "example": exampledata} - actionlist.push(actionvalue) + var foundSlice = false + for (let j = 0; j < actionlist.length; j++) { + //console.log("ACTION: ", found[i], actionlist[j]) + //console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase()) + if(found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()){ + //console.log("Found: ", found[i]) + // Validate path? + + foundSlice = true + } + } + + if (!foundSlice) { + if (!helperText.includes("Invalid variables")) { + helperText+= "Invalid variables: " + } + helperText+= found[i] + ", " + } } + } catch (e) { + console.log("Parsing error: ", e) + } + } + + if (looperText.length > 0) { + if (helperText.length > 0) { + helperText += ". " } - setActionlist(actionlist) + helperText += looperText } - }) - const changeActionParameter = (event, count, data) => { - //console.log(event) - if (data.name.startsWith("${") && data.name.endsWith("}")) { + return helperText + } + + const changeActionParameter = (event, count, data, viewForceUpdate) => { + //console.log("Action change: ", selectedAction, data) + if (data.name.startsWith("${") && data.name.endsWith("}")) { + // 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) { + // Escapes all double quotes + var toReplace = event.target.value.trim() + + + if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) { + toReplace = toReplace.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) { + paramcheck["value_replace"].push({ + key: data.name, + value: toReplace, + }); + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace; + } + + console.log("IN ELSE: ", paramcheck); + } + //console.log("PARAM: ", paramcheck) + //if (paramcheck.id === undefined) { + // console.log("Normal paramcheck") + //} else { + // selectedActionParameters[count]["value_replace"] = paramcheck + // selectedAction.parameters[count]["value_replace"] = paramcheck + //} + + if (paramcheck["value_replace"] === undefined) { + selectedActionParameters[count]["value_replace"] = paramcheck; + selectedAction.parameters[count]["value_replace"] = paramcheck; + } else { + selectedActionParameters[count]["value_replace"] = + paramcheck["value_replace"]; + selectedAction.parameters[count]["value_replace"] = + paramcheck["value_replace"]; + } + console.log("RESULT: ", selectedAction); + setSelectedAction(selectedAction); + //setUpdate(Math.random()) + return; + } + } + + + if (event.target.value[event.target.value.length - 1] === "$") { + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); + } + } else { + if (showDropdown) { + setShowDropdown(false); + } + } + + // bad detection mechanism probably + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + console.log("GET THE LAST ARGUMENT FOR NODE!"); + // THIS IS AN EXAMPLE OF SHOWING IT + /* + + const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} + setJsonList(GetParsedPaths(inputdata, "")) + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + console.log(jsonList) + */ + + // Search for the item backwards + // 1. Reverse search backwards from . -> $ + // 2. Search the actionlist for the item + // 3. Find the data for the specific item + + var curstring = ""; + var record = false; + for (let [key,keyval] in Object.entries(selectedActionParameters[count].value)) { + const item = selectedActionParameters[count].value[key]; + if (record) { + curstring += item; + } + + if (item === "$") { + record = true; + curstring = ""; + } + } + + //console.log("CURSTRING: ", curstring) + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase(); + var actionItem = actionlist.find( + (data) => + data.autocomplete.split(" ").join("_").toLowerCase() === curstring + ); + if (actionItem !== undefined) { + console.log("Found item: ", actionItem); + + //actionItem.example = actionItem.example.trim() + //actionItem.example = actionItem.example.split(" None").join(" \"None\"") + //actionItem.example = actionItem.example.split("\'").join("\"") + + var jsonvalid = true; + try { + const tmp = String(JSON.parse(actionItem.example)); + if ( + !actionItem.example.includes("{") && + !actionItem.example.includes("[") + ) { + jsonvalid = false; + } + } catch (e) { + jsonvalid = false; + } + + if (jsonvalid) { + setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")); + + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); + } + } + } + } + } else { + if (jsonList.length > 0) { + setJsonList([]); + } + } + + //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; + + 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") { + console.log("APIKEY - this shouldn't show up!") + } + + 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("#")) { + const splitparsed = parsedvalue.split(".#.") + //console.log("Cant contain #: ", splitparsed) + if (splitparsed.length > 1) { + data.value = splitparsed[0] + + selectedActionParameters[count].value = splitparsed[0] + selectedAction.parameters[count].value = splitparsed[0] + + //changeActionParameter({target: {value: splitparsed[1]}}, + selectedActionParameters[1].value = splitparsed[1] + selectedAction.parameters[1].value = splitparsed[1] + forceUpdate = true + } + } + } + + setSelectedAction(selectedAction); + if (forceUpdate || viewForceUpdate === true) { + setUpdate(Math.random()) + } + //setUpdate(event.target.value) + }; + + + const changeActionParameterCodeMirror = (event, count, data) => { + if (data.startsWith("${") && data.endsWith("}")) { // 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) { @@ -430,7 +909,7 @@ const ParsedAction = (props) => { var curstring = "" var record = false - for (var key in selectedActionParameters[count].value) { + for (let [key,keyval] in Object.entries(selectedActionParameters[count].value)) { const item = selectedActionParameters[count].value[key] if (record) { curstring += item @@ -481,838 +960,1574 @@ const ParsedAction = (props) => { } } - //console.log("CHANGING ACTION COUNT !") - selectedActionParameters[count].value = event.target.value - selectedAction.parameters[count].value = event.target.value + selectedActionParameters[count].autocompleted = false + selectedAction.parameters[count].autocompleted = false + selectedActionParameters[count].value = data + selectedAction.parameters[count].value = data setSelectedAction(selectedAction) //setUpdate(Math.random()) //setUpdate(event.target.value) } - const changeActionParameterCodemirror = (event, count, data) => { - console.log(event) - if (data.name.startsWith("${") && data.name.endsWith("}")) { - // 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) { - // 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) + const changeActionParameterVariable = (fieldvalue, count) => { + //console.log("CALLED THIS ONE WITH VALUE!", fieldvalue) + //if (selectedVariableParameter === fieldvalue) { + // return + //} - } else { - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - "key": data.name, - "value": toReplace, - }) - } else { - paramcheck["value_replace"][subparamindex]["value"] = toReplace - } + setSelectedVariableParameter(fieldvalue); - console.log("IN ELSE: ", paramcheck) - } - //console.log("PARAM: ", paramcheck) - //if (paramcheck.id === undefined) { - // console.log("Normal paramcheck") - //} else { - // selectedActionParameters[count]["value_replace"] = paramcheck - // selectedAction.parameters[count]["value_replace"] = paramcheck - //} + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; - if (paramcheck["value_replace"] === undefined) { - selectedActionParameters[count]["value_replace"] = paramcheck - selectedAction.parameters[count]["value_replace"] = paramcheck - } else { - selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] - selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] - } - console.log("RESULT: ", selectedAction) - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - return + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(fieldvalue); + }; + + // Sets ACTION_RESULT things + const changeActionParameterActionResult = (fieldvalue, count) => { + //cy.nodes().forEach(function( ele ) { + // if (ele.data()["label"] === fieldvalue) { + // selectedActionParameters[count].action_field = ele.id() + // return + // } + //}); + + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; + + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; + + const changeActionParameterVariant = (data, count) => { + selectedActionParameters[count].variant = data; + selectedActionParameters[count].value = ""; + + if (data === "ACTION_RESULT" && getParents !== undefined) { + var parents = getParents(selectedAction); + if (parents.length > 0) { + selectedActionParameters[count].action_field = parents[0].label; + } else { + selectedActionParameters[count].action_field = ""; + } + } else if (data === "WORKFLOW_VARIABLE") { + if ( + workflow.workflow_variables !== null && + workflow.workflow_variables !== undefined && + workflow.workflow_variables.length > 0 + ) { + selectedActionParameters[count].action_field = + workflow.workflow_variables[0].name; + } + } + + selectedAction.parameters = selectedActionParameters; + + // This is a stupid workaround to make it refresh rofl + setSelectedAction({}); + + if (setSelectedTrigger !== undefined) { + setSelectedTrigger({}); + setSelectedApp({}); + setSelectedEdge({}); + } + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; + + + const returnHelperText = (name, value) => { + if (name === undefined || value === undefined) { + return "" + } + + var helperText = "" + //console.log("DATA: ", name, value) + if (name.includes("url")) { + if (value.includes("localhost") || value.includes("127.0.0.1")) { + helperText = "Can't use localhost. Please change to an external IP or hostname." } } - if (event.display.maxLine.text[event.display.maxLine.text.length-1] === "$") { - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - } else { - if (showDropdown) { - setShowDropdown(false) - } - } - - // bad detection mechanism probably - if (event.display.maxLine.text[event.display.maxLine.text.length-1] === "." && actionlist.length > 0) { - console.log("GET THE LAST ARGUMENT FOR NODE!") - // THIS IS AN EXAMPLE OF SHOWING IT - /* - const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} - setJsonList(GetParsedPaths(inputdata, "")) - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - console.log(jsonList) - */ - - // Search for the item backwards - // 1. Reverse search backwards from . -> $ - // 2. Search the actionlist for the item - // 3. Find the data for the specific item - - var curstring = "" - var record = false - for (var key in selectedActionParameters[count].value) { - const item = selectedActionParameters[count].value[key] - if (record) { - curstring += item - } - - if (item === "$") { - record = true - curstring = "" - } - } - - //console.log("CURSTRING: ", curstring) - if (curstring.length > 0 && actionlist !== null) { - // Search back in the action list - curstring = curstring.split(" ").join("_").toLowerCase() - var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) - if (actionItem !== undefined) { - console.log("Found item: ", actionItem) - - //actionItem.example = actionItem.example.trim() - //actionItem.example = actionItem.example.split(" None").join(" \"None\"") - //actionItem.example = actionItem.example.split("\'").join("\"") - - var jsonvalid = true - try { - const tmp = String(JSON.parse(actionItem.example)) - if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - - if (jsonvalid) { - setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")) - - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - } - } - } - } else { - if (jsonList.length > 0) { - setJsonList([]) - } - } - - selectedActionParameters[count].value = event.display.maxLine.text - selectedAction.parameters[count].value = event.display.maxLine.text - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - //setUpdate(event.target.value) + return helperText } - const changeActionParameterVariable = (fieldvalue, count) => { - //console.log("CALLED THIS ONE WITH VALUE!", fieldvalue) - //if (selectedVariableParameter === fieldvalue) { - // return - //} - - setSelectedVariableParameter(fieldvalue) - - selectedActionParameters[count].action_field = fieldvalue - selectedAction.parameters = selectedActionParameters - - setSelectedApp(selectedApp) - setSelectedAction(selectedAction) - setUpdate(fieldvalue) - } - - // Sets ACTION_RESULT things - const changeActionParameterActionResult = (fieldvalue, count) => { - //cy.nodes().forEach(function( ele ) { - // if (ele.data()["label"] === fieldvalue) { - // selectedActionParameters[count].action_field = ele.id() - // return - // } - //}); - - selectedActionParameters[count].action_field = fieldvalue - selectedAction.parameters = selectedActionParameters - - // FIXME - check if startnode - - // Set value - setSelectedApp(selectedApp) - - setSelectedAction(selectedAction) - setUpdate(Math.random()) - } - - const changeActionParameterVariant = (data, count) => { - selectedActionParameters[count].variant = data - selectedActionParameters[count].value = "" - - if (data === "ACTION_RESULT") { - var parents = getParents(selectedAction) - if (parents.length > 0) { - selectedActionParameters[count].action_field = parents[0].label - } else { - selectedActionParameters[count].action_field = "" - } - } else if (data === "WORKFLOW_VARIABLE") { - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - selectedActionParameters[count].action_field = workflow.workflow_variables[0].name - } - } - - selectedAction.parameters = selectedActionParameters - - // This is a stupid workaround to make it refresh rofl - setSelectedAction({}) - - if (setSelectedTrigger !== undefined) { - setSelectedTrigger({}) - setSelectedApp({}) - setSelectedEdge({}) - } - // FIXME - check if startnode - - // Set value - setSelectedApp(selectedApp) - setSelectedAction(selectedAction) - setUpdate(Math.random()) - } - - // FIXME: Issue #40 - selectedActionParameters not reset - if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { - var authWritten = false + //console.log("AUTH: ", authenticationType) + if (authenticationType !== undefined && authenticationType !== null && authenticationType.type === "oauth2") { + /* return ( -
- Parameters - {selectedActionParameters.map((data, count) => { - if (data.variant === "") { - data.variant = "STATIC_VALUE" - } + + You must authenticate before using oauth2 apps. + + ) + */ + } + //console.log("APP: ", selectedApp) - // selectedAction.selectedAuthentication = e.target.value - // selectedAction.authentication_id = e.target.value.id - if (!selectedAction.auth_not_required && selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { - // This sets the placeholder in the frontend. (Replaced in backend) - selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name] - selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name] - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - - if (authWritten) { - return null - } + // FIXME: Issue #40 - selectedActionParameters not reset + if ( + Object.getOwnPropertyNames(selectedAction).length > 0 && + selectedActionParameters.length > 0 + ) { + var authWritten = false; + return ( +
+ + + + {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? +
+ + Select an app you want to use + + { + console.log("LABEL: ", option) + if ( + option === undefined || + option === null || + option.app_name === undefined || + option.app_name === null + ) { + return null; + } - authWritten = true - return ( - - Authentication fields are hidden - - ) - } + const newname = ( + option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={selectedAction.matching_actions} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + console.log("SELECT: ", event, newValue) + // Workaround with event lol + //if (newValue !== undefined && newValue !== null) { + // setNewSelectedAction({ target: { value: newValue.name } }); + //} + }} + renderOption={(data) => { + var newActionname = data.app_name; + if ( + data.label !== undefined && + data.label !== null && + data.label.length > 0 + ) { + newActionname = data.label; + } - var staticcolor = "inherit" - var actioncolor = "inherit" - var varcolor = "inherit" - var multiline - if (data.multiline !== undefined && data.multiline !== null && data.multiline === true) { - multiline = true - } + const iconInfo = GetIconInfo({ name: data.app_name }); + const useIcon = iconInfo.originalIcon; - if (data.value !== undefined && data.value !== null && data.value.startsWith("{") && data.value.endsWith("}")) { - multiline = true - } + newActionname = ( + newActionname.charAt(0).toUpperCase() + + newActionname.substring(1) + ).replaceAll("_", " "); - var placeholder = "Static value" - if (data.example !== undefined && data.example !== null && data.example.length > 0) { - placeholder = data.example - - if (data.name === "url" && data.value.length === 0) { - data.value = data.example - } - } - - if (data.name.startsWith("${") && data.name.endsWith("}")) { - const paramcheck = selectedAction.parameters.find(param => param.name === "body") - if (paramcheck !== undefined && paramcheck !== null) { - if (paramcheck["value_replace"] !== undefined && paramcheck["value_replace"] !== null) { - //console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"]) - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex !== -1) { - data.value = paramcheck["value_replace"][subparamindex]["value"] - } - } - } - } - - var disabled = false - var rows = "5" - var openApiHelperText = "This is an OpenAPI specific field" - if (selectedApp.generated && data.name === "url" && data.required && data.configuration && hideExtraTypes) { - console.log("GENERATED WITH DATA: ", data) - return null - } - - if (selectedApp.generated && data.name === "headers") { - //console.log("HEADER: ", data) - //if (data.value.length === 0) { - - //} - //setSelectedActionParameters(selectedActionParameters) - } - - if (selectedApp.generated && data.name === "body") { - const regex = /\${(\w+)}/g - const found = placeholder.match(regex) - if (found === null) { - //setExtraBodyFields([]) - } else { - rows = "1" - disabled = true - openApiHelperText = "OpenAPI spec: fill the following fields." - //console.log("SHOULD ADD TO selectedActionParameters!: ", found, selectedActionParameters) - var changed = false - for (var specKey in found) { - const tmpitem = found[specKey] - var skip = false - for (var innerkey in selectedActionParameters) { - if (selectedActionParameters[innerkey].name === tmpitem) { - skip = true - break + return ( +
+ + {useIcon} + + {newActionname} +
+ ); + }} + 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 + } } } - if (skip) { - //console.log("SKIPPING ", tmpitem) - continue - } + return ( + + ); + }} + /> +
+ : null} + {selectedAction.description !== undefined && selectedAction.description !== null && selectedAction.description.length > 0 && hiddenDescription === false ? ( +
+ + Description + + + {selectedAction.description} + +
+ ) : null} + {selectedActionParameters.map((data, count) => { + if (data.variant === "") { + data.variant = "STATIC_VALUE"; + } - changed = true - selectedActionParameters.push({ - action_field: "", - configuration: false, - description: "Generated by OpenAPI body example", - example: "", - id: "", - multiline: false, - name: tmpitem, - options: null, - required: false, - schema: {type: "string"}, - skip_multicheck: false, - tags: null, - value: "", - variant: "STATIC_VALUE", - }) - } + // selectedAction.selectedAuthentication = e.target.value + // selectedAction.authentication_id = e.target.value.id + if ( + !selectedAction.auth_not_required && + selectedAction.selectedAuthentication !== undefined && + selectedAction.selectedAuthentication.fields !== undefined && + selectedAction.selectedAuthentication.fields[data.name] !== + undefined + ) { + // This sets the placeholder in the frontend. (Replaced in backend) + selectedActionParameters[count].value = + selectedAction.selectedAuthentication.fields[data.name]; + selectedAction.parameters[count].value = + selectedAction.selectedAuthentication.fields[data.name]; + setSelectedAction(selectedAction); + //setUpdate(Math.random()) - if (changed) { - setSelectedActionParameters(selectedActionParameters) - } + if (authWritten) { + return null; + } - return + authWritten = true; + return ( + + Authentication fields are hidden + + ); + } + + // Added autofill to make this ALOT simpler + if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { + if (selectedActionParameters[count].length === 0) { + selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" + setSelectedAction(selectedAction) } + + return null } - const clickedFieldId = "rightside_field_"+count - // - - { - setMenuPosition({ - top: event.pageY+10, - left: event.pageX+10, - }) - setShowDropdownNumber(count) - setShowDropdown(true) - setShowAutocomplete(true) - }}/> - - - - ) - }} - fullWidth - multiline={multiline} - onClick={() => { - console.log("Clicked field: ", clickedFieldId) - setExpansionModalOpen(false) + var staticcolor = "inherit"; + var actioncolor = "inherit"; + var varcolor = "inherit"; + var multiline; + if ( + data.multiline !== undefined && + data.multiline !== null && + data.multiline === true + ) { + multiline = true; + } + + if ( + data.value !== undefined && + data.value !== null && + data.value.startsWith("{") && + data.value.endsWith("}") + ) { + multiline = true; + } + + var placeholder = "Value"; + if ( + data.example !== undefined && + data.example !== null && + data.example.length > 0 + ) { + placeholder = data.example; + + if (data.name === "url" && data.value !== undefined && data.value !== null && data.value.length === 0) { + data.value = data.example; + } + + // In case of data.example + if (data.value === undefined || data.value === null) { + data.value = "" + } + + if (data.value.length === 0) { + if (data.name.toLowerCase() === "headers") { + console.log("Should show headers field instead with + and -!") + + // Check if file ID exists + // + const fileFound = selectedActionParameters.find(param => param.name === "file_id") + if (fileFound === undefined || fileFound === null) { + data.value = data.example + } else { + // Purposely unset it if set by default when using files + data.value = "" + } + } + } + + /* + if (data.name !== "queries" && data.name !== "key" && data.name !== "value" ) { + data.value = data.example + } + } + */ + } + + if (data.name.startsWith("${") && data.name.endsWith("}")) { + const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); + + + if (paramcheck !== undefined && paramcheck !== null) { + if ( + paramcheck["value_replace"] !== undefined && + paramcheck["value_replace"] !== null + ) { + //console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"]) + const subparamindex = paramcheck["value_replace"].findIndex( + (param) => param.key === data.name + ); + if (subparamindex !== -1) { + data.value = + paramcheck["value_replace"][subparamindex]["value"]; + } + } + } + } + + var disabled = false; + var rows = "5"; + var openApiHelperText = "This is an OpenAPI specific field"; + /* + if ( + selectedApp.generated && + data.name === "url" && + data.required && + data.configuration + ) { + //&& + //hideExtraTypes + + //console.log("GENERATED WITH DATA: ", data); + return null; + } + */ + + if (selectedApp.generated && data.name === "headers") { + //console.log("HEADER: ", data) + //if (data.value.length === 0) { + //} + //setSelectedActionParameters(selectedActionParameters) + } + + var hideBodyButton = ""; + const hideBodyButtonValue = ( +
+ + { + var tag = "TOGGLED" + if (hideBody) { + tag = "UNTOGGLED" + } + + setHideBody(!hideBody); + + for (let paramkey in Object.entries(selectedActionParameters)) { + var currentItem = selectedActionParameters[paramkey]; + if (currentItem.name === "ssl_verify") { + + } + + if (currentItem.name === "body") { + // FIXME: Workaround for toggling, as actions don't have IDs. + // May screw up something in the future. + currentItem.id = tag + } + + if (currentItem.description === openApiFieldDesc) { + currentItem.field_active = !hideBody; + //console.log("Changing", currentItem); + } + } + }} + name="requires_unique" + /> + } + label={"Automatically fix body"} + /> + +
+ ) + + if (selectedApp.generated && data.name === "body") { + const regex = /\${(\w+)}/g; + const found = placeholder.match(regex); + + hideBodyButton = hideBodyButtonValue; + if (found === null || !hideBody) { + if (found === null) { + setActivateHidingBodyButton(true); + } else { + //console.log("In found: ", found, hideBody) + } + } else { + //console.log("SHOW BUTTON"); + + rows = "1"; + disabled = true; + openApiHelperText = "OpenAPI spec: fill the following fields."; + //console.log("SHOULD ADD TO selectedActionParameters!: ", found, selectedActionParameters) + var changed = false; + for (let specKey in found) { + const tmpitem = found[specKey]; + var skip = false; + + for (let innerkey in selectedActionParameters) { + if (selectedActionParameters[innerkey].name === tmpitem) { + skip = true; + break; + } + } + + if (skip) { + //console.log("SKIPPING ", tmpitem) + continue; + } + + changed = true; + selectedActionParameters.push({ + action_field: "", + configuration: false, + description: openApiFieldDesc, + example: "", + id: "", + multiline: true, + name: tmpitem, + options: null, + required: false, + schema: { type: "string" }, + skip_multicheck: false, + tags: null, + value: "", + variant: "STATIC_VALUE", + field_active: true, + }); + } + + if (changed) { + setSelectedActionParameters(selectedActionParameters); + } + + return hideBodyButton; + } + } + + if (activateHidingBodyButton === true) { + hideBodyButton = ""; + } + + const clickedFieldId = "rightside_field_" + count; + + const shufflecode = fieldCount !== count ? null : + ( + + ) + + // 0) { + baseHelperText = calculateHelpertext(data.value) + } + + + var tmpitem = data.name.valueOf(); + if (data.name.startsWith("${") && data.name.endsWith("}")) { + tmpitem = tmpitem.slice(2, data.name.length - 1); + } + + if (tmpitem === "from_shuffle") { + tmpitem = "from" + } + + tmpitem = ( + tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1) + ).replaceAll("_", " "); + + if (tmpitem === "Username basic") { + tmpitem = "Username" + } else if (tmpitem === "Password basic") { + tmpitem = "Password" + } + + multiline = data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline + + var datafield = ( + + + + { + event.preventDefault() + setFieldCount(count) + setcodedata(data.value) + setExpansionModalOpen(true) + }} + /> + + + { + event.preventDefault() + + // Get cursor position + // This makes it so we can put it in the right location? + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + + ), + }} + multiline={data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline} + helperText={returnHelperText(data.name, data.value)} + onClick={() => { + console.log("Clicked field: ", clickedFieldId, data.name) + + //if (data.name === "file_id") { + // console.log("show file video?") + // if (setShowVideo !== undefined) { + // setShowVideo("https://www.youtube.com/embed/DPYowyTbsSk") + // } + //} + //(data.name.toLowerCase().includes("api") || + /* + setExpansionModalOpen(false); + if ( + setScrollConfig !== undefined && + scrollConfig !== null && + scrollConfig !== undefined && + scrollConfig.selected !== clickedFieldId + ) { + scrollConfig.selected = clickedFieldId; + setScrollConfig(scrollConfig); + //console.log("Change field id!") + } + */ + + //console.log("Clicked field: ", clickedFieldId) if (setScrollConfig !== undefined && scrollConfig !== null && scrollConfig !== undefined && scrollConfig.selected !== clickedFieldId) { scrollConfig.selected = clickedFieldId setScrollConfig(scrollConfig) //console.log("Change field id!") } - }} - id={clickedFieldId} - rows={rows} - color="primary" - defaultValue={data.value} - //value={data.value} - //options={{ - // theme: 'gruvbox-dark', - // keyMap: 'sublime', - // mode: 'python', - //}} - //height={multiline ? 50 : 150} + }} + id={clickedFieldId} + rows={data.name.startsWith("${") && data.name.endsWith("}") ? 2 : rows} + color="primary" + defaultValue={data.value} + //value={data.value} + //options={{ + // theme: 'gruvbox-dark', + // keyMap: 'sublime', + // mode: 'python', + //}} + //height={multiline ? 50 : 150} - type={placeholder.includes("***") || (data.configuration && (data.name.toLowerCase().includes("api") || data.name.toLowerCase().includes("key") || data.name.toLowerCase().includes("pass"))) ? "password" : "text"} - placeholder={placeholder} - - onChange={(event) => { - //changeActionParameterCodemirror(event, count, data) - changeActionParameter(event, count, data) - }} - - helperText={selectedApp.generated && selectedApp.activated && data.name === "body" ? - - {openApiHelperText} - - : - data.name.startsWith("${") && data.name.endsWith("}") ? - - OpenAPI helperfield - - : - null + type={ + placeholder.includes("***") || + (data.configuration && + (data.name.toLowerCase().includes("api") || + data.name.toLowerCase().includes("key") || + data.name.toLowerCase().includes("pass"))) + ? "password" + : "text" + } + placeholder={placeholder} + onChange={(event) => { + //changeActionParameterCodemirror(event, count, data) + changeActionParameter(event, count, data); + }} + helperText={baseHelperText.length > 0 ? baseHelperText : + selectedApp.generated && + selectedApp.activated && + data.name === "body" ? ( + + {openApiHelperText} + + ) : data.name.startsWith("${") && data.name.endsWith("}") ? + null : null + } + onBlur={(event) => { + baseHelperText = calculateHelpertext(event.target.value) + if (setLastSaved !== undefined) { + setLastSaved(false) + } + }} + /> + ); + + // Finds headers from a string to be used for autocompletion + const findHeaders = (inputdata) => { + var splitdata = inputdata.split("\n") + + var foundnewline = false + var allValues = [] + for (let [key,keyval] in Object.entries(splitdata)) { + const line = splitdata[key] + if (line === "") { + foundnewline = true + continue } - onBlur={(event) => { - // Super basic check - //if (event.target.value.startsWith("{")) { - // console.log("VALIDATING JSON") - // try { - // JSON.parse(event.target.value) - // } catch (e) { - // alert.error("Failed to parse json: ", e) - // } - //} - }} - /> - //console.log("FIELD VALUE: ", data.value) - //const regexp = new RegExp("\W+\.", "g") - //let match - //while ((match = regexp.exec(data.value)) !== null) { - // console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`); - //} + var splitvalue = "" + if (line.includes(":")) { + splitvalue = ":" + } - //const str = = data.value.search(submatch) - //console.log("FOUND? ", n) - //for (var key in keywords) { - // const keyword = keywords[key] - // if (data.value.includes(keyword)) { - // console.log("INCLUDED: ", keyword) - // } - //} + if (line.includes("=")) { + splitvalue = "=" + } - //const keywords = ["len", "lower", "upper", "trim", "split", "length", "number", "parse", "join"] - if (selectedActionParameters[count].schema !== undefined && selectedActionParameters[count].schema !== null && selectedActionParameters[count].schema.type === "file") { - datafield = - - - { - setMenuPosition({ - top: event.pageY+10, - left: event.pageX+10, - }) - setShowDropdownNumber(count) - setShowDropdown(true) - setShowAutocomplete(true) - }}/> - - + if (splitvalue.length === 0){ + allValues.push({ + key: line, + value: "", + }) + continue + } + + var splitKeys = line.split(splitvalue) + if (splitKeys.length > 1) { + allValues.push({ + key: splitKeys[0].trim(), + value: splitKeys[1].trim(), + }) + } else { + console.log("No keys for ", line) + } + } + + // Just add one + if (foundnewline) { + allValues.push({ + key: "", + value: "", + }) + } + + return allValues + } + + if (data.name.toLowerCase() === "headers") { + //var tmpheaders = findHeaders(data.value) + var tmpheaders = findHeaders(selectedActionParameters[count].value) + const tmpdatafield = +
+ {tmpheaders.map((inputdata, index) => { + const oldkey = inputdata.key + const oldval = inputdata.value + + return ( + +
+ { + console.log("Change from oldkey to new: ", oldkey, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + const tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key,keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key,keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldkey === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(e.target.value + line) + } else { + newarr.push(e.target.value + ": " + line) + } + } else { + newarr.push(line.replace(oldkey, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> + { + console.log("Change from oldval to new: ", oldval, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + var tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key,keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key,keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldval === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(line + e.target.value) + } else { + newarr.push(line + ": " + e.target.value) + } + } else { + newarr.push(line.replace(oldval, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> +
+
) - }} - fullWidth - multiline={multiline} - rows="5" - color="primary" - defaultValue={data.value} - type={"text"} - placeholder={"The file ID to get"} - id={"rightside_field_"+count} - onChange={(event) => { - changeActionParameter(event, count, data) - }} - onBlur={(event) => { - }} - /> - //const fileId = "6daabec1-892b-469c-b603-c902e47223a9" - //datafield = `SHOW FILES FROM OTHER NODES? Filename: ${selectedActionParameters[count].value}` - /* + })} + +
+ } + + //console.log("FIELD VALUE: ", data.value) + //const regexp = new RegExp("\W+\.", "g") + //let match + //while ((match = regexp.exec(data.value)) !== null) { + // console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`); + //} + + //const str = = data.value.search(submatch) + //console.log("FOUND? ", n) + //for (var key in keywords) { + // const keyword = keywords[key] + // if (data.value.includes(keyword)) { + // console.log("INCLUDED: ", keyword) + // } + //} + + // Basic helpertext + + if (files !== undefined && files !== null && data.name.toLowerCase() === "file_category") { + //selectedActionParameters[count].options.length > 0 + console.log("FileS: ", files) + if (files.namespaces !== undefined && files.namespaces !== null && files.namespaces.length > 0) { + data.options = files.namespaces + } + } + + //const keywords = ["len", "lower", "upper", "trim", "split", "length", "number", "parse", "join"] + if ( + selectedActionParameters[count].schema !== undefined && + selectedActionParameters[count].schema !== null && + selectedActionParameters[count].schema.type === "file" + ) { + datafield = ( + + + { + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + ), + }} + helperText={returnHelperText(data.name, data.value)} + fullWidth + multiline={multiline} + rows="5" + color="primary" + defaultValue={data.value} + type={"text"} + placeholder={"The file ID to get"} + id={"rightside_field_" + count} + onChange={(event) => { + changeActionParameter(event, count, data); + }} + onBlur={(event) => {}} + /> + ); + //const fileId = "6daabec1-892b-469c-b603-c902e47223a9" + //datafield = `SHOW FILES FROM OTHER NODES? Filename: ${selectedActionParameters[count].value}` + /* if (selectedActionParameters[count].value != fileId) { changeActionParameter(fileId, count, data) setUpdate(Math.random()) } */ - } else if (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0) { - if (selectedActionParameters[count].value === "" && selectedActionParameters[count].required) { - // Rofl, dirty workaround :) - const e = { - target: { - value: selectedActionParameters[count].options[0], - } - } + } else if ( + (data.options !== undefined && + data.options !== null && + data.options.length > 0) + || + (selectedActionParameters[count].options !== undefined && + selectedActionParameters[count].options !== null && + selectedActionParameters[count].options.length > 0) + ) { + const parsedoptions = data.options !== undefined && data.options !== null && data.options.length > 0 ? data.options : selectedActionParameters[count].options - changeActionParameter(e, count, data) - } + if (selectedActionParameters[count].value === "") { + // && selectedActionParameters[count].required) { + // Rofl, dirty workaround :) + const e = { + target: { + value: parsedoptions[0], + }, + }; - datafield = - + return ( + + {viewed_data} + + ); + } + )} + + ); + } else if (data.variant === "STATIC_VALUE") { + staticcolor = "#f85a3e"; + } - } else if (data.variant === "STATIC_VALUE") { - staticcolor = "#f85a3e" - } + if (data.field_active === false) { + return null; + } - // Shows nested list of nodes > their JSON lists - const ActionlistWrapper = (props) => { - const handleMenuClose = () => { - setShowAutocomplete(false) + // Shows nested list of nodes > their JSON lists + const ActionlistWrapper = (props) => { + const handleMenuClose = () => { + setShowAutocomplete(false); - if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === "$") { - setShowDropdown(false) - } + if ( + !selectedActionParameters[count].value[ + selectedActionParameters[count].value.length - 1 + ] === "$" + ) { + setShowDropdown(false); + } - setUpdate(Math.random()) - setMenuPosition(null) - } + setUpdate(Math.random()); + setMenuPosition(null); + }; - const handleItemClick = (values) => { - if (values === undefined || values === null || values.length === 0) { - return - } + const handleItemClick = (values) => { + console.log("In normal itemclick") + if (values === undefined ||values === null ||values.length === 0) { + return; + } - console.log("AUTOCOMPLETE1: ", values) + var toComplete = selectedActionParameters[count].value.trim() + .endsWith("$") + ? values[0].autocomplete + : "$" + values[0].autocomplete; - var toComplete = selectedActionParameters[count].value.trim().endsWith("$") ? values[0].autocomplete : "$"+values[0].autocomplete - toComplete = toComplete.toLowerCase().replaceAll(" ", "_") - console.log("AUTOCOMPLETE: ", toComplete) - for (var key in values) { - if (key == 0 || values[key].autocomplete.length === 0) { - continue - } + toComplete = toComplete.toLowerCase().replaceAll(" ", "_"); + for (let [key,keyval] in Object.entries(values)) { + if (key == 0 || values[key].autocomplete.length === 0) { + continue; + } - toComplete += values[key].autocomplete - } + toComplete += values[key].autocomplete; + } - // 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) { - paramcheck["value_replace"] = [{ - "key": data.name, - "value": toComplete, - }] + - } else { - 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 + // 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 + ) { + paramcheck["value_replace"] = [ + { + key: data.name, + value: toComplete, + }, + ]; + } else { + 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; + } + } + + selectedActionParameters[count]["value_replace"] = paramcheck; + selectedAction.parameters[count]["value_replace"] = paramcheck; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + return; + } + } + + console.log("In nestedclick!!") + var newValue = selectedActionParameters[count].value + toComplete + changeActionParameter({target: {value: newValue}}, count, data, true) + //selectedActionParameters[count].value += toComplete; + //selectedAction.parameters[count].value = selectedActionParameters[count].value; + //setSelectedAction(selectedAction); + //setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + }; + + const iconStyle = { + marginRight: 15, + }; + + return ( + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + color: "white", + marginTop: 2, + maxHeight: 650, + }} + > + {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 [key,keyval] in Object.entries(workflow.triggers)) { + const item = workflow.triggers[key]; + + 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 = []; + if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + //menuPosition.left -= 50 + //menuPosition.top -= 250 + //console.log("POS: ", menuPosition1) + var menuPosition1 = menuPosition + if (menuPosition1 === null) { + menuPosition1 = { + "left": 0, + "top": 0, } + } else if (menuPosition1.top === null || menuPosition1.top === undefined) { + menuPosition1.top = 0 + } else if (menuPosition1.left === null || menuPosition1.left === undefined) { + menuPosition1.left = 0 } - - selectedActionParameters[count]["value_replace"] = paramcheck - selectedAction.parameters[count]["value_replace"] = paramcheck - setSelectedAction(selectedAction) - setUpdate(Math.random()) + //console.log("POS1: ", menuPosition1) - setShowDropdown(false) - setMenuPosition(null) - return - } - } + return parsedPaths.length > 0 ? ( + + {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" ? ( + + ) : ( + + ); + // - selectedActionParameters[count].value += toComplete - selectedAction.parameters[count].value = selectedActionParameters[count].value - setSelectedAction(selectedAction) - setUpdate(Math.random()) + 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" ? { + e.preventDefault() + e.stopPropagation() - setShowDropdown(false) - setMenuPosition(null) - } + console.log("INNER: ", innerdata, pathdata) + + // Removing .list from autocomplete + var newname = pathdata.name + if (newname.length > 5) { + newname = newname.slice(0, newname.length-5) + } + selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` + selectedAction.parameters[count].value = selectedActionParameters[count].value; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + setShowDropdown(false); + setMenuPosition(null); - const iconStyle = { - marginRight: 15, - } + // innerdata.name + // pathdata.name + //handleItemClick([innerdata, newpathdata]) + //console.log("CLICK LENGTH!") + }} /> : null} +
+
+
+ ); + })} + + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + +
+ {icon} {innerdata.name} +
+
+
+ ); + })} + + ); + }; + + const description = + data.description === undefined ? "" : data.description; + const tooltipDescription = ( + + + - Required:{" "} + {data.required === true || data.configuration === true + ? "True" + : "False"} + + + - Example: {data.example} + + + - Description: {description} + + + ); - return ( - { - handleMenuClose() - }} - open={!!menuPosition} - style={{ - color: "white", - marginTop: 2, - maxHeight: 650, - }} - > - {actionlist.map(innerdata => { - const icon = innerdata.type === "action" ? : innerdata.type === "workflow_variable" || innerdata.type === "execution_variable" ? : + //var itemColor = "#f85a3e" + //if (!data.required) { + // itemColor = "#ffeb3b" + //} + { + /*
*/ + } - 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 = "" - } - } + //console.log(data.configuration) - // Also doing arguments - if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > 0) { - for (var key in workflow.triggers) { - const item = workflow.triggers[key] + const buttonTitle = `Authenticate ${selectedApp.name.replaceAll("_", " ")}` + const hasAutocomplete = data.autocompleted === true + return ( +
+ {hideBodyButton} +
+ {data.configuration === true ? ( + + { + setAuthenticationModalOpen(true); + }} + /> + + ) : null} - 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 = [] - if (typeof(innerdata.example) === "object") { - parsedPaths = GetParsedPaths(innerdata.example, "") - } - - return ( - parsedPaths.length > 0 ? - - {icon} {innerdata.name} -
- } - parentMenuOpen={!!menuPosition} - style={{backgroundColor: theme.palette.inputColor, color: "white", minWidth: 250, maxWidth: 250, maxHeight: 650, scrollX: "", }} - //PaperProps={{ - // style: { - // maxHeight: 400, - // width: 250, - // } - //}} - onClick={() => { - console.log("CLICKED: ", innerdata) - handleItemClick([innerdata]) - }} - > - {parsedPaths.map((pathdata, index) => { - // FIXME: Should be recursive in here - const icon = pathdata.type === "value" ? : pathdata.type === "list" ? : - return ( - {console.log("HOVER: ", pathdata)}} - onClick={() => { - handleItemClick([innerdata, pathdata]) - }} - > - -
- {icon} {pathdata.name} -
-
-
- ) - - })} - + {hasAutocomplete === true ? + + + : - handleMouseover()} onMouseOut={() => {handleMouseOut()}} - onClick={() => { - handleItemClick([innerdata]) - }} - > - -
- {icon} {innerdata.name} -
-
-
- - ) - })} -
- ) - } + null} +
+ + {tmpitem} + +
- var tmpitem = data.name.valueOf() - if (data.name.startsWith("${") && data.name.endsWith("}")) { - tmpitem = tmpitem.slice(2, data.name.length-1) - } - - tmpitem = (tmpitem.charAt(0).toUpperCase()+tmpitem.substring(1)).replaceAll("_", " ") - - if (tmpitem === "Username basic") { - tmpitem = "Username" - } else if (tmpitem === "Password basic") { - tmpitem = "Password" - } - - const description = data.description === undefined ? "" : data.description - const tooltipDescription = - - - Required: {data.required === true || data.configuration === true ? "True" : "False"} - - Example: {data.example} - - Description: {description} - - - //var itemColor = "#f85a3e" - //if (!data.required) { - // itemColor = "#ffeb3b" - //} - {/*
*/} - return ( -
-
- {data.configuration === true ? - - { - setAuthenticationModalOpen(true) - }}/> - - : - null - } -
- - {tmpitem} - -
- - {/*selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null : + {/*selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null :
{ @@ -1342,7 +2557,7 @@ const ParsedAction = (props) => {
*/} - {/*(selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 && selectedActionParameters[count].required === true && selectedActionParameters[count].unique_toggled !== undefined) || hideExtraTypes ? null : + {/*(selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 && selectedActionParameters[count].required === true && selectedActionParameters[count].unique_toggled !== undefined) || hideExtraTypes ? null :
{}}> @@ -1355,7 +2570,7 @@ const ParsedAction = (props) => { onChange={(event) => { //console.log("CHECKED!: ", selectedActionParameters[count]) selectedActionParameters[count].unique_toggled = !selectedActionParameters[count].unique_toggled - selectedAction.parameters[count].unique_toggled = selectedActionParameters[count].unique_toggled + selectedAction.parameters[count].unique_toggled = selectedActionParameters[count].unique_toggled setSelectedActionParameters(selectedActionParameters) setSelectedAction(selectedAction) setUpdate(Math.random()) @@ -1366,158 +2581,307 @@ const ParsedAction = (props) => {
*/} -
- {datafield} - {showDropdown && showDropdownNumber === count && data.variant === "STATIC_VALUE" && jsonList.length > 0 ? - - Autocomplete - { + setShowAutocomplete(false); + + if ( + !selectedActionParameters[count].value[ + selectedActionParameters[count].value.length - 1 + ] === "." + ) { + setShowDropdown(false); + } + + setUpdate(Math.random()); + }} + onClick={() => { + setShowAutocomplete(true) + }} + fullWidth + open={showAutocomplete} + style={{ + color: "white", + height: 50, + marginTop: 2, + borderRadius: theme.palette.borderRadius, + }} + onChange={(e) => { + if (selectedActionParameters[count].value[selectedActionParameters[count].value.length - 1] === ".") { + e.target.value.autocomplete = e.target.value.autocomplete.slice(1,e.target.value.autocomplete.length); + } + + selectedActionParameters[count].value += e.target.value.autocomplete; + selectedAction.parameters[count].value = selectedActionParameters[count].value; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + + setShowDropdown(false); + }} + > + {jsonList.map((data) => { + const iconStyle = { + marginRight: 15, + }; + + const icon = + data.type === "value" ? ( + + ) : data.type === "list" ? ( + + ) : ( + + ); + return ( + {}} + > + +
+ {icon} {data.name} +
+
+
+ ); + })} + +
+ ) : null} + {showDropdown && + showDropdownNumber === count && + data.variant === "STATIC_VALUE" && + jsonList.length === 0 ? ( + + ) : null} +
+ ); + })} +
+ ); + } + return null; + }; + + //const CustomPopper = function (props) { + // const classes = useStyles() + // return + //} + //console.log("env: ", selectedActionEnvironment) + + var baselabel = selectedAction.label; + return ( +
+ + {hideExtraTypes === true ? null : ( + +
+
+

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

+
+ { + console.log("FIND EXAMPLE RESULTS FOR ", selectedAction); + 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; + } + + // Enforces it to show at least one + //if (workflowExecutions[key].execution_argument.includes("too large") && key !== workflowExecutions.length - 1) { + // 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); + console.log("FOUND NODe: ", oldstartnode) + 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); + } + + break; + } + } + }} + > + + + + + { + 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 } - }} - onClose={() => { - setShowAutocomplete(false) - - if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === ".") { - setShowDropdown(false) - } - - setUpdate(Math.random()) - }} - onClick={() => setShowAutocomplete(true)} - fullWidth - open={showAutocomplete} - style={{color: "white", height: 50, marginTop: 2, borderRadius: theme.palette.borderRadius,}} - onChange={(e) => { - if (selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === ".") { - e.target.value.autocomplete = e.target.value.autocomplete.slice(1, e.target.value.autocomplete.length) - } - - selectedActionParameters[count].value += e.target.value.autocomplete - selectedAction.parameters[count].value = selectedActionParameters[count].value - setSelectedAction(selectedAction) - setUpdate(Math.random()) - - setShowDropdown(false) - }} - > - {jsonList.map(data => { - const iconStyle = { - marginRight: 15, - } - - const icon = data.type === "value" ? : data.type === "list" ? : - return ( - {}}> - -
- {icon} {data.name} -
-
-
- ) - })} - - - : null} - {showDropdown && showDropdownNumber === count && data.variant === "STATIC_VALUE" && jsonList.length === 0 ? - - : null} - - -
- )})} -
- ) - } - return null - } - - const expansionModal = - { - setExpansionModalOpen(false) - }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: 600, - padding: 50, - }, - }} - > - Workflow Variable - - Hello - - - - //const CustomPopper = function (props) { - // const classes = useStyles() - // return - //} - - const baselabel = selectedAction.label - return ( -
- {expansionModal} - {hideExtraTypes === true ? null : - -
-
-

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

-
- { - console.log("FIND EXAMPLE RESULTS FOR ", selectedAction) - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false - for (var key in 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 - } + setSelectedAction(selectedAction) + setUpdate(Math.random()); + }} + > + + + + + */} + { + }} + > + + + + + + - setSelectedResult(foundResult) +
+
+
+ {/*selectedAction.id === workflow.start ? null : - if (setCodeModalOpen !== undefined) { - setCodeModalOpen(true) - } - break - } - } - }}> - - - - - { - setAuthenticationModalOpen(true) - }}> - - - - - {}}> - - - - - - -
-
-
- {/*selectedAction.id === workflow.start ? null : */} - {selectedApp.versions !== null && selectedApp.versions !== undefined && selectedApp.versions.length > 1 ? - { + console.log("VAL: ", event.target.value) + console.log("App: ", selectedApp) + const newversion = selectedApp.versions.find( + (tmpApp) => tmpApp.version == event.target.value + ); - - ) - })} - - : null } -
-
- - - Name - - { - const name = e.target.value - console.log("CHANGED FROM2: ", baselabel) - console.log("CHANGED TO: ", name) - for (var key in workflow.actions) { - for (var subkey in workflow.actions[key].parameters) { - const param = workflow.actions[key].parameters[subkey] - if (param.value.includes(baselabel)) { - //if (param.value.toLowerCase().includes(baselabel)) { - console.log("FOUND: ", param) - workflow.actions[key].parameters[subkey].value.replaceAll(baselabel, e.target.value) - } - } - } - - console.log("DID REPLACE ACTUALLY WORK?? - Something is buggy.") - setWorkflow(workflow) - }} - /> - - } - {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 + console.log("NEWVERSION: ", newversion); + if (newversion !== undefined && newversion !== null) { + getApp(newversion.id, true); + } + }} + style={{ + marginTop: 10, + backgroundColor: theme.palette.surfaceColor, + backgroundColor: theme.palette.inputColor, + color: "white", + height: 35, + marginleft: 10, + borderRadius: theme.palette.borderRadius, + }} + SelectDisplayProps={{ + style: { + marginLeft: 10, + }, + }} + > + {selectedApp.versions.map((data, index) => { + return ( + + {data.version} + + ); + })} + + ) : null} +
+
+
- + labelId="select-app-auth" + value={ + Object.getOwnPropertyNames( + selectedAction.selectedAuthentication + ).length === 0 + ? "No selection" + : selectedAction.selectedAuthentication + } + SelectDisplayProps={{ + style: { + marginLeft: 10, + maxWidth: 250, + }, + }} + fullWidth + onChange={(e) => { + if (e.target.value === "No selection") { + selectedAction.selectedAuthentication = {}; + selectedAction.authentication_id = ""; - - No selection - - {selectedAction.authentication.map(data => { - //console.log("AUTH DATA: ", data) - return( - - {data.label} - ({data.app.app_version}) - - ) - })} - + for (let [key,keyval] in Object.entries(selectedAction.parameters)) { + //console.log(selectedAction.parameters[key]) + if (selectedAction.parameters[key].configuration) { + selectedAction.parameters[key].value = ""; + } + } + setSelectedAction(selectedAction); + setUpdate(Math.random()); + } else { + selectedAction.selectedAuthentication = e.target.value; + selectedAction.authentication_id = e.target.value.id; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + } + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + height: 50, + maxWidth: rightsidebarStyle.maxWidth - 80, + borderRadius: theme.palette.borderRadius, + }} + > + + No selection + + {selectedAction.authentication.map((data) => { + //console.log("AUTH DATA: ", data) + return ( + + {data.label} - ({data.app.app_version}) + + ); + })} + - {/* + {/*
-
- : null} + + { + setAuthenticationModalOpen(true); + }} + > + + + +
+
+ ) : null} - {showEnvironment !== undefined && showEnvironment ? -
- - Environment - - { + const env = environments.find((a) => a.Name === e.target.value); + setSelectedActionEnvironment(env); + selectedAction.environment = env.Name; + setSelectedAction(selectedAction); + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + height: "50px", + borderRadius: theme.palette.borderRadius, + }} + > + {environments.map((data, index) => { + if (data.archived === true) { + return null; + } - } - }} - fullWidth - onChange={(e) => { - const env = environments.find(a => a.Name === e.target.value) - setSelectedActionEnvironment(env) - selectedAction.environment = env.Name - setSelectedAction(selectedAction) - }} - style={{backgroundColor: theme.palette.inputColor, color: "white", height: "50px", borderRadius: theme.palette.borderRadius,}} - > - {environments.map(data => { - if (data.archived) { - return null - } - - return ( - - {data.Name} - - ) - })} - -
- : null} + return ( + + {data.Name} + + ); + })} + +
+ ) : null} - {workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ? -
- - Set execution variable (optional) - - { - if (e.target.value === "No selection") { - selectedAction.execution_variable = {"name": "No selection"} - } else { - const value = workflow.execution_variables.find(a => a.name === e.target.value) - selectedAction.execution_variable = value - } - setSelectedAction(selectedAction) - setUpdate(Math.random()) - }} - style={{backgroundColor: theme.palette.inputColor, color: "white", height: "50px", borderRadius: theme.palette.borderRadius,}} - > - - No selection - - - {workflow.execution_variables.map(data => ( - - {data.name} - - ))} - -
- : null} + value={ + selectedAction.execution_variable !== undefined + && selectedAction.execution_variable !== null + && selectedAction.execution_variable.name !== undefined + && selectedAction.execution_variable.name !== null + && selectedAction.execution_variable.name.length > 0 + ? selectedAction.execution_variable.name + : "No selection" + } + SelectDisplayProps={{ + style: { + marginLeft: 10, + }, + }} + fullWidth + onChange={(e) => { + if (e.target.value === "No selection") { + selectedAction.execution_variable = { name: "No selection" }; + } else { + const value = workflow.execution_variables.find( + (a) => a.name === e.target.value + ); + selectedAction.execution_variable = value; + } + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + height: "50px", + borderRadius: theme.palette.borderRadius, + }} + > + + No selection + + + {workflow.execution_variables.map((data) => ( + + {data.name} + + ))} + +
+ ) : null} - -
- {/*hideExtraTypes ? 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"; }} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.name === undefined || option.name === undefined) { - return null - } - - const newname = (option.name.charAt(0).toUpperCase()+option.name.substring(1)).replaceAll("_", " ") - return newname - }} - options={sortByKey(selectedApp.actions, "label")} - 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={(data) => { - var newActionname = data.name - if (data.label !== undefined && data.label !== null && data.label.length > 0) { - newActionname = data.label - } - - const iconInfo = GetIconInfo({"name": data.name}) - const useIcon = iconInfo.originalIcon - - newActionname = (newActionname.charAt(0).toUpperCase()+newActionname.substring(1)).replaceAll("_", " ") - + renderGroup={(params) => { return ( -
- {useIcon} - {newActionname} -
- ) +
  • + {params.group} + {params.children} +
  • + ) + }} + options={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"))} + ListboxProps={{ + style: { + backgroundColor: theme.palette.inputColor, + 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 }} - renderInput={(params) => { - return ( - { + 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={(data) => { + 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; + + 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 ( + +
    +
    + + {useIcon} + + {newActionname} +
    + {extraDescription.length > 0 ? + + {extraDescription} + + : null} +
    +
    + ); + }} + 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 + } + } + } + + return ( + - ) - }} - /> + ); + }} + /> + ) : null} - : null} - - {/*setNewSelectedAction !== undefined ? + {/*setNewSelectedAction !== undefined ? : null*/} - {selectedAction.description !== undefined && selectedAction.description.length > 0 && hideExtraTypes !== true ? -
    - {selectedAction.description} -
    : null} - -
    - -
    -
    -
    - ) - } - +
    + +
    +
    +
    + ); +}; export default ParsedAction; diff --git a/frontend/src/components/RenderCytoscape.js b/frontend/src/components/RenderCytoscape.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/ScrollToTop.jsx b/frontend/src/components/ScrollToTop.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/components/SettingsPopup.js b/frontend/src/components/SettingsPopup.js old mode 100644 new mode 100755 diff --git a/frontend/src/components/history.jsx b/frontend/src/components/history.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/css/font1.woff2 b/frontend/src/css/font1.woff2 old mode 100644 new mode 100755 diff --git a/frontend/src/css/font2.woff2 b/frontend/src/css/font2.woff2 old mode 100644 new mode 100755 diff --git a/frontend/src/css/font3.woff2 b/frontend/src/css/font3.woff2 old mode 100644 new mode 100755 diff --git a/frontend/src/css/font4.woff2 b/frontend/src/css/font4.woff2 old mode 100644 new mode 100755 diff --git a/frontend/src/css/font5.woff2 b/frontend/src/css/font5.woff2 old mode 100644 new mode 100755 diff --git a/frontend/src/css/nunito.css b/frontend/src/css/nunito.css old mode 100644 new mode 100755 diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js old mode 100644 new mode 100755 diff --git a/frontend/src/index.css b/frontend/src/index.css old mode 100644 new mode 100755 diff --git a/frontend/src/index.js b/frontend/src/index.js old mode 100644 new mode 100755 diff --git a/frontend/src/serviceWorker.js b/frontend/src/serviceWorker.js old mode 100644 new mode 100755 diff --git a/frontend/src/theme.js b/frontend/src/theme.js old mode 100644 new mode 100755 diff --git a/frontend/src/views/About.jsx b/frontend/src/views/About.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx old mode 100644 new mode 100755 index e0169439..2082cf2a --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1,120 +1,267 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; -import { makeStyles } from '@material-ui/styles'; -import { useTheme } from '@material-ui/core/styles'; -import {Link} from 'react-router-dom'; +import { makeStyles } from "@material-ui/styles"; +import { useNavigate, Link } from "react-router-dom"; +import countries from "../components/Countries.jsx"; +import CodeEditor from "../components/ShuffleCodeEditor.jsx"; +import getLocalCodeData from "../components/ShuffleCodeEditor.jsx"; +import CacheView from "../components/CacheView.jsx"; +import theme from "../theme.jsx"; +import AddIcon from "@mui/icons-material/Add"; +import ClearIcon from '@mui/icons-material/Clear'; +import StorageIcon from '@mui/icons-material/Storage'; +//import ToggleButton from '@mui/material/ToggleButton'; +import { + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Typography, + Switch, + Select, + MenuItem, + Divider, + TextField, + Button, + ButtonGroup, + Tabs, + Tab, + Grid, + List, + ListItem, + ListItemText, + ListItemAvatar, + ListItemSecondaryAction, + IconButton, + Avatar, + Zoom, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + CircularProgress, + Box, + InputAdornment, +} from "@material-ui/core"; -import {FormControl, InputLabel, Paper, Card, Tooltip, FormControlLabel, Typography, Switch, Select, MenuItem, Divider, TextField, Button, Tabs, Tab, Grid, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction, IconButton, Avatar, Zoom, Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress } from '@material-ui/core'; +import { Autocomplete } from "@mui/material"; -import {Edit as EditIcon, FileCopy as FileCopyIcon, Publish as PublishIcon, SelectAll as SelectAllIcon, OpenInNew as OpenInNewIcon, CloudDownload as CloudDownloadIcon, Description as DescriptionIcon, Polymer as PolymerIcon, CheckCircle as CheckCircleIcon, Close as CloseIcon, Apps as AppsIcon, Image as ImageIcon, Delete as DeleteIcon, Cached as CachedIcon, AccessibilityNew as AccessibilityNewIcon, Lock as LockIcon, Eco as EcoIcon, Schedule as ScheduleIcon, Cloud as CloudIcon, Business as BusinessIcon} from '@material-ui/icons'; +import { + Edit as EditIcon, + FileCopy as FileCopyIcon, + SelectAll as SelectAllIcon, + OpenInNew as OpenInNewIcon, + CloudDownload as CloudDownloadIcon, + Description as DescriptionIcon, + Polymer as PolymerIcon, + CheckCircle as CheckCircleIcon, + Close as CloseIcon, + Apps as AppsIcon, + Image as ImageIcon, + Delete as DeleteIcon, + Cached as CachedIcon, + AccessibilityNew as AccessibilityNewIcon, + Lock as LockIcon, + Eco as EcoIcon, + Schedule as ScheduleIcon, + Cloud as CloudIcon, + Business as BusinessIcon, + Visibility as VisibilityIcon, + VisibilityOff as VisibilityOffIcon, +} from "@material-ui/icons"; import { useAlert } from "react-alert"; -import Dropzone from '../components/Dropzone'; -import HandlePayment from './HandlePayment' -import OrgHeader from '../components/OrgHeader' +import Dropzone from "../components/Dropzone.jsx"; +import HandlePaymentNew from "../views/HandlePaymentNew.jsx"; +import OrgHeader from "../components/OrgHeader.jsx"; +import OrgHeaderexpanded from "../components/OrgHeaderexpanded.jsx"; +import Billing from "../components/Billing.jsx"; +import Priorities from "../components/Priorities.jsx"; +import Branding from "../components/Branding.jsx"; +import Files from "../components/Files.jsx"; +import { display, style } from "@mui/system"; +//import EnvironmentStats from "../components/EnvironmentStats.jsx"; const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important" - }, -}) + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 500, + }, + }, + getContentAnchorEl: () => null, +}; + + +const FileCategoryInput = (props) => { + const isSet = props.isSet; + console.log("inside filecategoryinput"); + console.log("isset value" , isSet); + if (isSet){ + return ( + + )} + } + const Admin = (props) => { - const { globalUrl, userdata } = props; + const { globalUrl, userdata, serverside, checkLogin } = props; - var upload = "" - var to_be_copied = "" - const theme = useTheme(); - const classes = useStyles(); - const [firstRequest, setFirstRequest] = React.useState(true); - const [orgRequest, setOrgRequest] = React.useState(true); - const [modalUser, setModalUser] = React.useState({}); - const [orgName, setOrgName] = React.useState("") - const [modalOpen, setModalOpen] = React.useState(false); + var to_be_copied = ""; + const classes = useStyles(); + let navigate = useNavigate(); - const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false); - const [cloudSyncApikey, setCloudSyncApikey] = React.useState(""); - const [loading, setLoading] = React.useState(false); + const [firstRequest, setFirstRequest] = React.useState(true); + const [orgRequest, setOrgRequest] = React.useState(true); + const [modalUser, setModalUser] = React.useState({}); + const [orgName, setOrgName] = React.useState(""); + const [modalOpen, setModalOpen] = React.useState(false); - const [selectedOrganization, setSelectedOrganization] = React.useState({}); - const [organizationFeatures, setOrganizationFeatures] = React.useState({}); - const [loginInfo, setLoginInfo] = React.useState(""); - const [curTab, setCurTab] = React.useState(0); - const [users, setUsers] = React.useState([]); - const [organizations, setOrganizations] = React.useState([]); - const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); - const [userSettings, setUserSettings] = React.useState({}); + const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false); + const [cloudSyncApikey, setCloudSyncApikey] = React.useState(""); + const [loading, setLoading] = React.useState(false); - const [environments, setEnvironments] = React.useState([]); - const [authentication, setAuthentication] = React.useState([]); - const [schedules, setSchedules] = React.useState([]) - const [files, setFiles] = React.useState([]) - const [selectedNamespace, setSelectedNamespace] = React.useState("default") - const [fileNamespaces, setFileNamespaces] = React.useState([]); - const [selectedUser, setSelectedUser] = React.useState({}) - const [newUsername, setNewUsername] = React.useState(""); - const [newPassword, setNewPassword] = React.useState(""); - const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false) - const [selectedAuthentication, setSelectedAuthentication] = React.useState({}) - const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false) - const [authenticationFields, setAuthenticationFields] = React.useState([]) - const [showArchived, setShowArchived] = React.useState(false) - const [isDropzone, setIsDropzone] = React.useState(false); + const [selectedOrganization, setSelectedOrganization] = React.useState({}); + + //console.log("Selected: ", selectedOrganization) + const [organizationFeatures, setOrganizationFeatures] = React.useState({}); + const [loginInfo, setLoginInfo] = React.useState(""); + const [curTab, setCurTab] = React.useState(0); + const [users, setUsers] = React.useState([]); + const [organizations, setOrganizations] = React.useState([]); + const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); + const [userSettings, setUserSettings] = React.useState({}); + const [matchingOrganizations, setMatchingOrganizations] = React.useState([]); - useEffect(() => { - if (isDropzone) { - //redirectOpenApi(); - setIsDropzone(false); - } + const [environments, setEnvironments] = React.useState([]); + const [authentication, setAuthentication] = React.useState([]); + const [schedules, setSchedules] = React.useState([]); + const [selectedUser, setSelectedUser] = React.useState({}); + const [newUsername, setNewUsername] = React.useState(""); + const [newPassword, setNewPassword] = React.useState(""); + const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}); + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [authenticationFields, setAuthenticationFields] = React.useState([]); + const [showArchived, setShowArchived] = React.useState(false); + const [isDropzone, setIsDropzone] = React.useState(false); + + const [image2FA, setImage2FA] = React.useState(""); + const [value2FA, setValue2FA] = React.useState(""); + const [secret2FA, setSecret2FA] = React.useState(""); + const [show2faSetup, setShow2faSetup] = useState(false); + + const [adminTab, setAdminTab] = React.useState(2); + const [showApiKey, setShowApiKey] = useState(false); + const [billingInfo, setBillingInfo] = React.useState({}); + const [selectedStatus, setSelectedStatus] = React.useState([]); + + useEffect(() => { + if (isDropzone) { + //redirectOpenApi(); + setIsDropzone(false); + } }, [isDropzone]); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" - const getApps = () => { - fetch(globalUrl+"/api/v1/apps", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - } + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - return response.json() - }) - .then((responseJson) => { - console.log("apps: ", responseJson) - //setApps(responseJson) - //setFilteredApps(responseJson) - //if (responseJson.length > 0) { - // setSelectedApp(responseJson[0]) - // if (responseJson[0].actions !== null && responseJson[0].actions.length > 0) { - // setSelectedAction(responseJson[0].actions[0]) - // } else { - // setSelectedAction({}) - // } - //} + const get2faCode = (userId) => { + fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .catch(error => { - alert.error(error.toString()) - }); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } - const categories = [ - { - "name": "Ticketing", - "apps": [ - "TheHive", - "Service-Now", - "SecureWorks", - ], - "categories": ["tickets", "ticket", "ticketing"] - }, - ] - /* + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + if (responseJson.success === true) { + //alert.info(responseJson.reason) + setImage2FA(responseJson.reason); + setSecret2FA(responseJson.extra); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const getApps = () => { + fetch(globalUrl + "/api/v1/apps", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("apps: ", responseJson); + //setApps(responseJson) + //setFilteredApps(responseJson) + //if (responseJson.length > 0) { + // setSelectedApp(responseJson[0]) + // if (responseJson[0].actions !== null && responseJson[0].actions.length > 0) { + // setSelectedAction(responseJson[0].actions[0]) + // } else { + // setSelectedAction({}) + // } + //} + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const categories = [ + { + name: "Ticketing", + apps: ["TheHive", "Service-Now", "SecureWorks"], + categories: ["tickets", "ticket", "ticketing"], + }, + ]; + /* "SIEM", "Active Directory", "Firewalls", @@ -134,2858 +281,4131 @@ const Admin = (props) => { ] */ - const alert = useAlert() + const alert = useAlert(); + const handleStatusChange = (event) => { + const { value } = event.target; - const deleteAuthentication = (data) => { - alert.info("Deleting auth " + data.label) + setSelectedStatus(value); + handleEditOrg( + "", + "", + selectedOrganization.id, + "", + {}, + {}, + value, + ) + } - // Just use this one? - const url = globalUrl + '/api/v1/apps/authentication/' + data.id - console.log("URL: ", url) - fetch(url, { - method: 'DELETE', - credentials: "include", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - console.log("RESP: ", responseJson) - if (responseJson["success"] === false) { - alert.error("Failed deleting auth") + // Basically just a simple way to get a generated email + // This also may help understand how to communicate with users + // both inside and outside Shuffle + // This could also be generated on the backend + const mailsendingButton = (org) => { + if (org === undefined || org === null) { + return "" + } + + if (users.length === 0) { + return "" + } + + // 1 mail based on users that have only apps + // Another based on those doing workflows + // Another based on those trying usecases(?) or templates + // + // Start based on edr, siem & ticketing + // Talk about enrichment? + // Check suggested usecases + // Check suggested workflows + var your_apps = "- Connecting " + + var subject_add = 0 + var subject = "Want to automate " + + if (org.security_framework !== undefined && org.security_framework !== null) { + if (org.security_framework.cases.name !== undefined && org.security_framework.cases.name !== null && org.security_framework.cases.name !== "") { + your_apps += org.security_framework.cases.name.replace("_", " ", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " & " + } + + subject_add += 1 + subject += org.security_framework.cases.name.replace("_", " ", -1) + } + } + + if (org.security_framework.siem.name !== undefined && org.security_framework.siem.name !== null && org.security_framework.siem.name !== "") { + your_apps += org.security_framework.siem.name.replace("_", " ", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " & " + } + + subject_add += 1 + subject += org.security_framework.siem.name.replace("_", " ", -1) + } + } + + if (org.security_framework.communication.name !== undefined && org.security_framework.communication.name !== null && org.security_framework.communication.name !== "") { + your_apps += org.security_framework.communication.name.replace("_", " ", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " & " + } + + subject_add += 1 + subject += org.security_framework.communication.name.replace("_", " ", -1) + } + } + + if (org.security_framework.edr.name !== undefined && org.security_framework.edr.name !== null && org.security_framework.edr.name !== "") { + your_apps += org.security_framework.edr.name.replace("_", " ", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " & " + } + + subject_add += 1 + subject += org.security_framework.edr.name.replace("_", " ", -1) + } + } + + if (org.security_framework.intel.name !== undefined && org.security_framework.intel.name !== null && org.security_framework.intel.name !== "") { + your_apps += org.security_framework.intel.name.replace("_", " ", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " & " + } + + subject_add += 1 + subject += org.security_framework.intel.name.replace("_", " ", -1) + } + } + + + // Remove comma + subject += "?" + your_apps = your_apps.substring(0, your_apps.length - 2) + } + + + // Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase + var usecases = "- Building usecases like " + const active_usecase = org.priorities.filter((item) => item.type === "usecase" && item.active === true) + if (active_usecase.length > 0) { + for (var i = 0; i < active_usecase.length; i++) { + if (active_usecase[i].name.includes("Suggested Usecase: ")) { + usecases += active_usecase[i].name.replace("Suggested Usecase: ", "", -1) + ", " } else { - // Need to wait because query in ES is too fast - setTimeout(() => { - getAppAuthentication() - }, 1000) - //alert.success("Successfully deleted authentication!") + usecases += active_usecase[i].name + ", " } - }), - ) - .catch(error => { - console.log("Error in userdata: ", error) - }); - } + } - const deleteSchedule = (data) => { - // FIXME - add some check here ROFL - console.log("INPUT: ", data) - - // Just use this one? - const url = globalUrl + '/api/v1/workflows/' + data["workflow_id"] + "/schedule/" + data.id - console.log("URL: ", url) - fetch(url, { - method: 'DELETE', - credentials: "include", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - console.log("RESP: ", responseJson) - if (responseJson["success"] === false) { - alert.error("Failed stopping schedule") - } else { - setTimeout(() => { - getSchedules() - }, 1500) - //alert.success("Successfully stopped schedule!") - } - }), - ) - .catch(error => { - console.log("Error in userdata: ", error) - }); - } - - const handleStopOrgSync = (org_id) => { - if (org_id === undefined || org_id === null) { - alert.error("Couldn't get org "+org_id) - return + usecases = usecases.substring(0, usecases.length - 2) } - const data = {} + if (your_apps.length <= 15) { + your_apps = "" + } - const url = globalUrl + '/api/v1/orgs/' + org_id + "/stop_sync"; - 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 => { - if (response.status === 200) { - console.log("Cloud sync success?") - alert.success("Successfully stopped cloud sync") - } else { - console.log("Cloud sync fail?") - alert.error("Failed stopping sync. Try again, and contact support if this persists.") - } - - return response.json() - }) - .then((responseJson) => { - setTimeout(() => { - handleGetOrg(org_id) - }, 1000) - }) - .catch(error => { - alert.error("Err: " + error.toString()) - }) - } - - const enableCloudSync = (apikey, organization, disableSync) => { - setOrgSyncResponse("") - - const data = { - apikey: apikey, - organization: organization, - disable: disableSync, + if (usecases.length <= 30) { + usecases = "" } - const url = globalUrl + '/api/v1/cloud/setup'; - 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 => { - setLoading(false) - if (response.status === 200) { - console.log("Cloud sync success?") - } else { - console.log("Cloud sync fail?") + var workflow_amount = "a few" + var admins = "" + + // Loop users + for (var i = 0; i < users.length; i++) { + if (users[i].role === "admin") { + admins += users[i].username + "," } - - return response.json() - //setTimeout(() => { - //}, 1000) - }) - .then((responseJson) => { - console.log("RESP: ", responseJson) - if (responseJson.success === false && responseJson.reason !== undefined) { - setOrgSyncResponse(responseJson.reason) - alert.error("Failed to handle sync: "+responseJson.reason) - } else if (!responseJson.success) { - alert.error("Failed to handle sync.") - } else { - getOrgs() - if (disableSync) { - alert.success("Successfully disabled sync!") - setOrgSyncResponse("Successfully disabled syncronization") - } else { - alert.success("Cloud Syncronization successfully set up!") - setOrgSyncResponse("Successfully started syncronization. Cloud features you now have access to can be seen below.") - } - - selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync - setSelectedOrganization(selectedOrganization) - setCloudSyncApikey("") - - handleGetOrg(userdata.active_org.id) - } - }) - .catch(error => { - setLoading(false) - alert.error("Err: " + error.toString()) - }) - } - - const saveAuthentication = (authentication) => { - const data = authentication - const url = globalUrl + '/api/v1/apps/authentication'; - - fetch(url, { - mode: 'cors', - method: 'PUT', - 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) { - alert.error("Failed changing authentication") - } else { - //alert.success("Successfully password!") - setSelectedUserModalOpen(false) - getAppAuthentication() - } - }), - ) - .catch(error => { - alert.error("Err: " + error.toString()) - }); - } - - const editAuthenticationConfig = (id) => { - const data = { - "id": id, - "action": "assign_everywhere", - } - const url = globalUrl + '/api/v1/apps/authentication/'+id+"/config"; - - 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) { - alert.error("Failed overwriting appauth in workflows") - } else { - alert.success("Successfully updated auth everywhere!") - setSelectedUserModalOpen(false) - setTimeout(() => { - getAppAuthentication() - }, 1000) - } - }), - ) - .catch(error => { - alert.error("Err: " + error.toString()) - }); - } - - const createSubOrg = (currentOrgId, name) => { - const data = { "name": name, "org_id": currentOrgId} - console.log(data) - const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org` - - 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) { - if (responseJson.reason !== undefined) { - alert.error(responseJson.reason) - } else { - alert.error("Failed creating suborg") - } - } else { - alert.success("Successfully created suborg!") - setSelectedUserModalOpen(false) - } - - setOrgName("") - setModalOpen(false) - }), - ) - .catch(error => { - alert.error("Err: " + error.toString()) - }); - - } - - const onPasswordChange = () => { - const data = { "username": selectedUser.username, "newpassword": newPassword } - const url = globalUrl + '/api/v1/users/passwordchange'; - - 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) { - if (responseJson.reason !== undefined) { - alert.error(responseJson.reason) - } else { - alert.error("Failed setting new password") - } - } else { - alert.success("Successfully updated password!") - setSelectedUserModalOpen(false) - } - }), - ) - .catch(error => { - alert.error("Err: " + error.toString()) - }); - } - - const deleteUser = (data) => { - // Just use this one? - const userId = data.id - - const url = globalUrl + '/api/v1/users/' + userId - fetch(url, { - method: 'DELETE', - credentials: "include", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => { - if (response.status === 200) { - getUsers() - } - - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success && responseJson.reason !== undefined) { - alert.error("Failed to deactivate user: "+responseJson.reason) - } else { - alert.success("Deactivated user "+data.id) - } - }) - - .catch(error => { - console.log("Error in userdata: ", error) - }); - } - - const handleGetOrg = (orgId) => { - if (orgId.length === 0) { - alert.error("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.") - return } - // Just use this one? - var baseurl = globalUrl - const url = baseurl + '/api/v1/orgs/'+orgId - fetch(url, { - method: 'GET', - credentials: "include", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => { - if (response.status === 401) { - } + // Remove last comma + admins = admins.substring(0, admins.length - 1) - return response.json() - }) - .then(responseJson => { - if (responseJson["success"] === false) { - alert.error("Failed getting org: ", responseJson.readon) - } else { - if (responseJson.sync_features === undefined || responseJson.sync_features === null) { - responseJson.sync_features = {} - - } - setSelectedOrganization(responseJson) - var lists = { - "active": { - "triggers": [], - "features": [], - "sync": [], - }, - "inactive": { - "triggers": [], - "features": [], - "sync": [], - }, - } - - - // FIXME: Set up features - //Object.keys(responseJson.sync_features).map(function(key, index) { - // //console.log(responseJson.sync_features[key]) - //}) - - //setOrgName(responseJson.name) - //setOrgDescription(responseJson.description) - setOrganizationFeatures(lists) - } - }) - .catch(error => { - console.log("Error getting org: ", error) - alert.error("Error getting current organization") - }); - } - - const inviteUser = (data) => { - console.log("INPUT: ", data) - setLoginInfo("") - - // Just use this one? - var data = { "username": data.Username, "type": "invite", "org_id": selectedOrganization.id} - var baseurl = globalUrl - const url = baseurl + '/api/v1/users/register_org'; - - fetch(url, { - method: 'POST', - credentials: "include", - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - setLoginInfo("Error: " + responseJson.reason) - } else { - setLoginInfo("") - setModalOpen(false) - setTimeout(() => { - getUsers() - }, 1000) - } - }), - ) - .catch(error => { - console.log("Error in userdata: ", error) - }); - } - - const submitUser = (data) => { - console.log("INPUT: ", data) - setLoginInfo("") - - // Just use this one? - var data = { "username": data.Username, "password": data.Password } - var baseurl = globalUrl - const url = baseurl + '/api/v1/users/register'; - - fetch(url, { - method: 'POST', - credentials: "include", - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - setLoginInfo("Error: " + responseJson.reason) - } else { - setLoginInfo("") - setModalOpen(false) - setTimeout(() => { - getUsers() - }, 1000) - } - }), - ) - .catch(error => { - console.log("Error in userdata: ", error) - }); - } - - // Horrible frontend fix for environments - const setDefaultEnvironment = (environment) => { - // FIXME - add more checks to this - alert.info("Setting default env to " + environment.name) - var newEnv = [] - for (var key in environments) { - if (environments[key].id == environment.id) { - if (environments[key].archived) { - alert.error("Can't set archived to default") - return - } - - environments[key].default = true - } else if (environments[key].default == true && environments[key].id !== environment.id) { - environments[key].default = false - } - - newEnv.push(environments[key]) + if (your_apps.length > 5) { + your_apps += "%0D%0A" } - // Just use this one? - const url = globalUrl + '/api/v1/setenvironments'; - fetch(url, { - method: 'PUT', - credentials: "include", - body: JSON.stringify(newEnv), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - alert.error(responseJson.reason) - setTimeout(() => { - getEnvironments() - }, 1500) - } else { - setLoginInfo("") - setModalOpen(false) - setTimeout(() => { - getEnvironments() - }, 1500) - } - }), - ) - .catch(error => { - console.log("Error in backend data: ", error) - }) - } - - const flushQueue = (name) => { - // Just use this one? - const url = globalUrl + '/api/v1/flush_queue'; - fetch(url, { - method: 'DELETE', - credentials: "include", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - alert.error(responseJson.reason) - getEnvironments() - } else { - setLoginInfo("") - setModalOpen(false) - getEnvironments() - } - }), - ) - .catch(error => { - console.log("Error when deleting: ", error) - }) - } - - const deleteEnvironment = (environment) => { - // FIXME - add some check here ROFL - //const name = environment.name - - //alert.info("Modifying environment " + name) - //var newEnv = [] - //for (var key in environments) { - // if (environments[key].Name == name) { - // if (environments[key].default) { - // alert.error("Can't modify the default environment") - // return - // } - - // if (environments[key].type === "cloud" && !environments[key].archived) { - // alert.error("Can't modify cloud environments") - // return - // } - - // environments[key].archived = !environments[key].archived - // } - - // newEnv.push(environments[key]) - //} - const id = environment.id - - //alert.info("Modifying environment " + environment.Name) - var newEnv = [] - for (var key in environments) { - if (environments[key].id == id) { - if (environments[key].default) { - alert.error("Can't modify the default environment") - return - } - - if (environments[key].type === "cloud" && !environments[key].archived) { - alert.error("Can't modify cloud environments") - return - } - - environments[key].archived = !environments[key].archived - } - - newEnv.push(environments[key]) + if (usecases.length > 5) { + usecases += "%0D%0A" } - // Just use this one? - const url = globalUrl + '/api/v1/setenvironments'; - fetch(url, { - method: 'PUT', - credentials: "include", - body: JSON.stringify(newEnv), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - alert.error(responseJson.reason) - getEnvironments() - } else { - setLoginInfo("") - setModalOpen(false) - getEnvironments() - } - }), - ) - .catch(error => { - console.log("Error when deleting: ", error) - }) + // Get drift username from userdata.username before @ in email + const username = userdata.username.substring(0, userdata.username.indexOf("@")) + + var body = `Hey,%0D%0A%0D%0AI saw you trying to use Shuffle, and thought we may be able to help. Right now, it looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting the most out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A + +Some of the things we can help with:%0D%0A +${your_apps} +- Configuring and authenticating your apps%0D%0A +${usecases} +- Creating special usecases and apps%0D%0A%0D%0A + +Let me know if you're interested, or set up a call here: https://drift.me/${username}` + + return `mailto:${admins}?bcc=frikky@shuffler.io&subject=${subject}&body=${body}` } - const submitEnvironment = (data) => { - // FIXME - add some check here ROFL - environments.push({ - "name": data.environment, - "type": "onprem", - }) + const deleteAuthentication = (data) => { + alert.info("Deleting auth " + data.label); - // Just use this one? - var baseurl = globalUrl - const url = baseurl + '/api/v1/setenvironments'; - fetch(url, { - method: 'PUT', - credentials: "include", - body: JSON.stringify(environments), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - setLoginInfo("Error in input: " + responseJson.reason) - getEnvironments() - } else { - setLoginInfo("") - setModalOpen(false) - getEnvironments() - } - }), - ) - .catch(error => { - console.log("Error in userdata: ", error) - }); - } - - const handleFileUpload = (file_id, file) => { - //console.log("FILE: ", file_id, file) - fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, { - method: 'POST', - credentials: "include", - body: file, - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - //console.log("RESPONSE: ", responseJson) - //setFiles(responseJson) - }) - .catch(error => { - //alert.error(error.toString()) - }); - } - - const handleCreateFile = (filename, file) => { - const data = { - "filename": filename, - "org_id": selectedOrganization.id, - "workflow_id": "global", - } - - fetch(globalUrl + "/api/v1/files/create", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - body: JSON.stringify(data), - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - //console.log("RESP: ", responseJson) - if (responseJson.success) { - handleFileUpload(responseJson.id, file) - } else { - alert.error("Failed to upload file ", filename) - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getFiles = () => { - fetch(globalUrl + "/api/v1/files", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.files !== undefined && responseJson.files !== null) { - setFiles(responseJson.files) - } else { - setFiles([]) - } - - console.log("NAMESPACES: ", responseJson.namespaces) - if (responseJson.namespaces !== undefined && responseJson.namespaces !== null) { - setFileNamespaces(responseJson.namespaces) - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const downloadFile = (file) => { - fetch(globalUrl + "/api/v1/files/"+file.id+"/content", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return "" - } - - return response.text() - }) - .then((respdata) => { - if (respdata.length === 0) { - alert.error("Failed getting file") - return - } - - var blob = new Blob( [ respdata ], { - type: 'application/octet-stream' - }) - - var url = URL.createObjectURL( blob ) - var link = document.createElement( 'a' ) - link.setAttribute( 'href', url ) - link.setAttribute( 'download', `${file.filename}` ) - var event = document.createEvent( 'MouseEvents' ) - event.initMouseEvent( 'click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null) - link.dispatchEvent( event ) - - //return response.json() - }) - .then((responseJson) => { - //console.log(responseJson) - //setSchedules(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getSchedules = () => { - fetch(globalUrl + "/api/v1/workflows/schedules", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - setSchedules(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getAppAuthentication = () => { - fetch(globalUrl + "/api/v1/apps/authentication", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.success) { - //console.log(responseJson.data) - //console.log(responseJson) - setAuthentication(responseJson.data) - } else { - alert.error("Failed getting authentications") - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getEnvironments = () => { - fetch(globalUrl + "/api/v1/getenvironments", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - setEnvironments(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getOrgs = () => { - // API no longer in use, as it's in handleInfo request - return - - fetch(globalUrl + "/api/v1/orgs", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - return - } - - return response.json() - }) - .then((responseJson) => { - setOrganizations(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getUsers = () => { - fetch(globalUrl + "/api/v1/getusers", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - // Ahh, this happens because they're not admin - // window.location.pathname = "/workflows" - return - } - - return response.json() - }) - .then((responseJson) => { - setUsers(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getSettings = () => { - fetch(globalUrl+"/api/v1/getsettings", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 when getting settings :O!") - } - - return response.json() - }) - .then((responseJson) => { - setUserSettings(responseJson) + // Just use this one? + const url = globalUrl + "/api/v1/apps/authentication/" + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, }) - .catch(error => { - console.log(error) - }); + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error("Failed deleting auth"); + } else { + // Need to wait because query in ES is too fast + setTimeout(() => { + getAppAuthentication(); + }, 1000); + //alert.success("Successfully deleted authentication!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const deleteSchedule = (data) => { + // FIXME - add some check here ROFL + console.log("INPUT: ", data); + + // Just use this one? + const url = + globalUrl + + "/api/v1/workflows/" + + data["workflow_id"] + + "/schedule/" + + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + console.log("RESP: ", responseJson); + if (responseJson["success"] === false) { + alert.error("Failed stopping schedule"); + } else { + setTimeout(() => { + getSchedules(); + }, 1500); + //alert.success("Successfully stopped schedule!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + + if (userdata.support === true && selectedOrganization.id !== "" && selectedOrganization.id !== undefined && selectedOrganization.id !== null && selectedOrganization.id !== userdata.active_org.id) { + alert.info("Refreshing window to fix org support access") + window.location.reload() + return null } - const views = { - 0: "organization", - 1: "users", - 2: "app_auth", - 3: "files", - 4: "schedules", - 5: "environments", - 6: "categories", - } - const setConfig = (event, newValue) => { - //console.log("Value: ", newValue) + const handleVerify2FA = (userId, code) => { + const data = { + code: code, + user_id: userId, + }; - setCurTab(parseInt(newValue)) - if (newValue === 1) { - document.title = "Shuffle - admin - users" - getUsers() - } else if (newValue === 2) { - document.title = "Shuffle - admin - app authentication" - getAppAuthentication() - } else if (newValue === 3) { - document.title = "Shuffle - admin - files" - getFiles() - } else if (newValue === 4) { - document.title = "Shuffle - admin - schedules" - getSchedules() - } else if (newValue === 5) { - document.title = "Shuffle - admin - environments" - getEnvironments() - } else if (newValue === 6) { - document.title = "Shuffle - admin - orgs" - getOrgs() - } else { - document.title = "Shuffle - admin" + fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + } else { + //alert.info("Wrong code sent.") + //alert.info("Wrong code sent. Please try again.") + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + alert.info("Successfully enabled 2fa"); + + setTimeout(() => { + getUsers(); + + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); + setSelectedUserModalOpen(false); + }, 1000); + } else { + alert.info("Wrong code sent. Please try again."); + //alert.error("Failed setting 2fa: ", responseJson.reason) + } + }) + .catch((error) => { + alert.info("Wrong code sent. Please try again."); + //alert.error("Err: " + error.toString()) + }); + }; + + const handleStopOrgSync = (org_id) => { + if (org_id === undefined || org_id === null) { + alert.error("Couldn't get org " + org_id); + return; + } + + const data = {}; + + const url = globalUrl + "/api/v1/orgs/" + org_id + "/stop_sync"; + 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) => { + if (response.status === 200) { + console.log("Cloud sync success?"); + alert.success("Successfully stopped cloud sync"); + } else { + console.log("Cloud sync fail?"); + alert.error( + "Failed stopping sync. Try again, and contact support if this persists." + ); + } + + return response.json(); + }) + .then((responseJson) => { + setTimeout(() => { + handleGetOrg(org_id); + }, 1000); + }) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const enableCloudSync = (apikey, organization, disableSync) => { + setOrgSyncResponse(""); + + const data = { + apikey: apikey, + organization: organization, + disable: disableSync, + }; + + const url = globalUrl + "/api/v1/cloud/setup"; + 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) => { + setLoading(false); + if (response.status === 200) { + console.log("Cloud sync success?"); + } else { + console.log("Cloud sync fail?"); + } + + return response.json(); + //setTimeout(() => { + //}, 1000) + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + setOrgSyncResponse(responseJson.reason); + alert.error("Failed to handle sync: " + responseJson.reason); + } else if (!responseJson.success) { + alert.error("Failed to handle sync."); + } else { + getOrgs(); + if (disableSync) { + alert.success("Successfully disabled sync!"); + setOrgSyncResponse("Successfully disabled syncronization"); + } else { + alert.success("Cloud Syncronization successfully set up!"); + setOrgSyncResponse( + "Successfully started syncronization. Cloud features you now have access to can be seen below." + ); + } + + selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync; + setSelectedOrganization(selectedOrganization); + setCloudSyncApikey(""); + + handleGetOrg(userdata.active_org.id); + } + }) + .catch((error) => { + setLoading(false); + alert.error("Err: " + error.toString()); + }); + }; + + const saveAuthentication = (authentication) => { + const data = authentication; + const url = globalUrl + "/api/v1/apps/authentication"; + + fetch(url, { + mode: "cors", + method: "PUT", + 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) { + alert.error("Failed changing authentication"); + } else { + //alert.success("Successfully password!") + setSelectedUserModalOpen(false); + getAppAuthentication(); + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config, + lead_info, + ) => { + + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + lead_info: lead_info, + }; + + 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) { + alert.error("Failed updating org: ", responseJson.reason); + } else { + if (lead_info === undefined || lead_info === null || lead_info === []) { + alert.success("Successfully edited org!"); + } + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const editAuthenticationConfig = (id) => { + const data = { + id: id, + action: "assign_everywhere", + }; + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; + + 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) { + alert.error("Failed overwriting appauth in workflows"); + } else { + alert.success("Successfully updated auth everywhere!"); + setSelectedUserModalOpen(false); + setTimeout(() => { + getAppAuthentication(); + }, 1000); + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const createSubOrg = (currentOrgId, name) => { + const data = { name: name, org_id: currentOrgId }; + console.log(data); + const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org`; + + 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) { + if (responseJson.reason !== undefined) { + alert.error(responseJson.reason); + } else { + alert.error("Failed creating suborg. Please try again"); + } + } else { + alert.success( + "Successfully created suborg. Reloading in 3 seconds!" + ); + setSelectedUserModalOpen(false); + + setTimeout(() => { + window.location.reload(); + }, 2500); + } + + setOrgName(""); + setModalOpen(false); + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const onPasswordChange = () => { + const data = { username: selectedUser.username, newpassword: newPassword }; + const url = globalUrl + "/api/v1/users/passwordchange"; + + 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) { + if (responseJson.reason !== undefined) { + alert.error(responseJson.reason); + } else { + alert.error("Failed setting new password"); + } + } else { + alert.success("Successfully updated password!"); + setSelectedUserModalOpen(false); + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const deleteUser = (data) => { + // Just use this one? + const userId = data.id; + + const url = globalUrl + "/api/v1/users/" + userId; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 200) { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed to deactivate user: " + responseJson.reason); + } else { + alert.success("Changed activation for user " + data.id); + } + }) + + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + + + const handleGetOrg = (orgId) => { + + if (serverside !== true && window.location.search !== undefined && window.location.search !== null) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundorgid = params["org_id"]; + if (foundorgid !== undefined && foundorgid !== null) { + orgId = foundorgid; + } } - if (newValue === 6) { - console.log("Should get apps for categories.") - } + if (orgId.length === 0) { + alert.error("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); + return; + } + + // Just use this one? + + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + alert.error("Failed getting your org. If this persists, please contact support."); + } else { + if ( + responseJson.sync_features === undefined || + responseJson.sync_features === null + ) { + responseJson.sync_features = {}; + } + + if (responseJson.lead_info !== undefined && responseJson.lead_info !== null) { + var leads = [] + if (responseJson.lead_info.contacted) { + leads.push("contacted") + } + + if (responseJson.lead_info.customer) { + leads.push("customer") + } + + if (responseJson.lead_info.demo_done) { + leads.push("demo done") + } + + if (responseJson.lead_info.pov) { + leads.push("pov") + } + + if (responseJson.lead_info.lead) { + leads.push("lead") + } + + if (responseJson.lead_info.student) { + leads.push("student") + } + + setSelectedStatus(leads) + } + + setSelectedOrganization(responseJson) + var lists = { + active: { + triggers: [], + features: [], + sync: [], + }, + inactive: { + triggers: [], + features: [], + sync: [], + }, + }; + + // FIXME: Set up features + //Object.keys(responseJson.sync_features).map(function(key, index) { + // //console.log(responseJson.sync_features[key]) + //}) + + //setOrgName(responseJson.name) + //setOrgDescription(responseJson.description) + setOrganizationFeatures(lists); + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + alert.error("Error getting current organization"); + }); + }; + + const inviteUser = (data) => { + //console.log("INPUT: ", data); + setLoginInfo(""); + + // Just use this one? + var data = { + username: data.Username, + type: "invite", + org_id: selectedOrganization.id, + }; + var baseurl = globalUrl; + const url = baseurl + "/api/v1/users/register_org"; + + fetch(url, { + method: "POST", + credentials: "include", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error: " + responseJson.reason); + alert.error("Failed to send email (2). Please try again and contact support if this persists.") + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getUsers(); + }, 1000); + + alert.info("Invite sent! They will show up in the list when they have accepted the invite.") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + alert.error("Failed to send email. Please try again and contact support if this persists.") + }); + }; + + const submitUser = (data) => { + console.log("INPUT: ", data); + setLoginInfo(""); + + // Just use this one? + var data = { username: data.Username, password: data.Password }; + var baseurl = globalUrl; + const url = baseurl + "/api/v1/users/register"; + + fetch(url, { + method: "POST", + credentials: "include", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error: " + responseJson.reason); + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getUsers(); + }, 1000); + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + // Horrible frontend fix for environments + const setDefaultEnvironment = (environment) => { + // FIXME - add more checks to this + alert.info("Setting default env to " + environment.name); + var newEnv = []; + for (var key in environments) { + if (environments[key].id == environment.id) { + if (environments[key].archived) { + alert.error("Can't set archived to default"); + return; + } + + environments[key].default = true; + } else if ( + environments[key].default == true && + environments[key].id !== environment.id + ) { + environments[key].default = false; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error(responseJson.reason); + setTimeout(() => { + getEnvironments(); + }, 1500); + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getEnvironments(); + }, 1500); + } + }) + ) + .catch((error) => { + console.log("Error in backend data: ", error); + }); + }; + + const flushQueue = (name) => { + // Just use this one? + const url = globalUrl + "/api/v1/flush_queue"; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }) + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + const rerunCloudWorkflows = (environment) => { + alert.info("Starting execution reruns. This can run in the background.") + fetch( + `${globalUrl}/api/v1/environments/${environment.id}/rerun`, + { + method: "GET", + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } else { + alert.error(response.reason); + //alert.info("Aborted all dangling workflows"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //alert.error(error.toString()) + }); + }; + + const abortEnvironmentWorkflows = (environment) => { + //console.log("Aborting all workflows started >10 minutes ago, not finished"); + + fetch( + `${globalUrl}/api/v1/environments/${environment.id}/stop?deleteall=true`, + { + method: "GET", + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + alert.error("Failed aborting dangling workflows"); + return; + } else { + alert.info("Aborted all dangling workflows"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //alert.error(error.toString()) + }); + }; + + const deleteEnvironment = (environment) => { + // FIXME - add some check here ROFL + //const name = environment.name + + //alert.info("Modifying environment " + name) + //var newEnv = [] + //for (var key in environments) { + // if (environments[key].Name == name) { + // if (environments[key].default) { + // alert.error("Can't modify the default environment") + // return + // } + + // if (environments[key].type === "cloud" && !environments[key].archived) { + // alert.error("Can't modify cloud environments") + // return + // } + + // environments[key].archived = !environments[key].archived + // } + + // newEnv.push(environments[key]) + //} + const id = environment.id; + + //alert.info("Modifying environment " + environment.Name) + var newEnv = []; + for (var key in environments) { + if (environments[key].id == id) { + if (environments[key].default) { + alert.error("Can't modify the default environment"); + return; + } + + if (environments[key].type === "cloud" && !environments[key].archived) { + alert.error("Can't modify cloud environments"); + return; + } + + environments[key].archived = !environments[key].archived; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }) + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + const submitEnvironment = (data) => { + // FIXME - add some check here ROFL + environments.push({ + name: data.environment, + type: "onprem", + }); + + // Just use this one? + var baseurl = globalUrl; + const url = baseurl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(environments), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error in input: " + responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + var localData = ""; - //var theURL = window.location.pathname - //FIXME: Add url edits - //var theURL = window.location - //theURL.replace(`/${views[curTab]}`, `/${views[newValue]}`) - //window.history.pushState({"html":response.html,"pageTitle":response.pageTitle},"", urlPath); + const getSchedules = () => { + fetch(globalUrl + "/api/v1/workflows/schedules", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } - //console.log(newpath) - //window.location.pathame = newpath + return response.json(); + }) + .then((responseJson) => { + setSchedules(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - setModalUser({}) - } + const getAppAuthentication = () => { + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success) { + //console.log(responseJson.data) + //console.log(responseJson) + setAuthentication(responseJson.data); + } else { + alert.error("Failed getting authentications"); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - if (firstRequest) { - setFirstRequest(false) - document.title = "Shuffle - admin" - if (!isCloud) { - getUsers() - } else { - getSettings() - } + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } - if (props.match.params.key !== undefined) { - //const tmpitem = views[props.match.params.key] - setConfig("", props.match.params.key) - } - } + return response.json(); + }) + .then((responseJson) => { + setEnvironments(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined && orgRequest) { - setOrgRequest(false) - handleGetOrg(userdata.active_org.id) - } + const getOrgs = () => { + // API no longer in use, as it's in handleInfo request + return; - const paperStyle = { - maxWidth: 1250, - margin: "auto", - color: "white", - backgroundColor: theme.palette.surfaceColor, - marginBottom: 10, - padding: 20, - } + fetch(globalUrl + "/api/v1/orgs", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } - const changeModalData = (field, value) => { - modalUser[field] = value - } + return response.json(); + }) + .then((responseJson) => { + setOrganizations(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - const setUser = (userId, field, value) => { - const data = { "user_id": userId } - data[field] = value + const getUsers = () => { + fetch(globalUrl + "/api/v1/getusers", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + // Ahh, this happens because they're not admin + // window.location.pathname = "/workflows" + return; + } - fetch(globalUrl + "/api/v1/users/updateuser", { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(data), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } else { - getUsers() - } + return response.json(); + }) + .then((responseJson) => { + setUsers(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success && responseJson.reason !== undefined) { - alert.error("Failed setting user: " + responseJson.reason) - } else { - alert.success("Set the user field " + field + " to " + value) - setSelectedUserModalOpen(false) - } - }) - .catch(error => { - console.log(error) - }); - } + useEffect(() => { + getUsers() + }, []); + const getSettings = () => { + fetch(globalUrl + "/api/v1/getsettings", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting settings :O!"); + } + return response.json(); + }) + .then((responseJson) => { + setUserSettings(responseJson); + }) + .catch((error) => { + console.log(error); + }); + }; - const generateApikey = (user) => { - const userId = user.id - const data = { "user_id": userId } + const views = { + 0: "organization", + 1: "users", + 2: "app_auth", + 3: "files", + 4: "cache", + 5: "schedules", + 6: "environments", + 7: "suborgs", + }; - fetch(globalUrl + "/api/v1/generateapikey", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(data), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } else { - getUsers() - } + const admin_views = { + 0: "organization", + 1: "cloud_sync", + 2: "priorities", + 3: "billing", + 4: "branding", + }; - return response.json() - }) - .then((responseJson) => { - console.log("RESP: ", responseJson) - if (!responseJson.success && responseJson.reason !== undefined) { - alert.error("Failed getting new: " + responseJson.reason) - } else { - alert.success("Got new API key") - } - }) - .catch(error => { - console.log(error) - }); - } + const setConfig = (event, inputValue) => { + const newValue = parseInt(inputValue); - const editAuthenticationModal = selectedAuthenticationModalOpen ? - { setSelectedAuthenticationModalOpen(false) }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - Edit authentication for {selectedAuthentication.app.name} ({selectedAuthentication.label}) - - {selectedAuthentication.fields.map((data, index) => { - console.log("DATA: ", data, selectedAuthentication) - return ( -
    - {data.key} - { - authenticationFields[index].value = e.target.value - setAuthenticationFields(authenticationFields) - }} - /> -
    - ) - })} -
    - - - - -
    - : null - - const editUserModal = - { setSelectedUserModalOpen(false) }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - Editing {selectedUser.username} - - {isCloud ? - null - : -
    - { - setNewUsername(e.target.value) - }} - /> - -
    } - {isCloud ? - null - : -
    - setNewPassword(e.target.value)} - /> - -
    - } - - - -
    -
    - - const GridItem = (props) => { - const [expanded, setExpanded] = React.useState(false) - - const primary = props.data.primary - const secondary = props.data.secondary - const primaryIcon = props.data.icon - const secondaryIcon = props.data.active ? - - : - - - return ( - { - setExpanded(!expanded) - }}> - - - - - {primaryIcon} - - - - {secondaryIcon} - - {expanded ? -
    - - Usage: {props.data.limit === 0 ? "Infinite" : {props.data.usage} / {props.data.limit}} - - - Data sharing: {props.data.data_collection} - - - Description: {secondary} - -
    - : null} -
    -
    - ) - } - - const itemColor = "black" - var syncList = [ - { - "primary": "Workflows", - "secondary": "", - "active": true, - "icon": , - }, - { - "primary": "Apps", - "secondary": "", - "active": true, - "icon": , - }, - { - "primary": "Organization", - "secondary": "", - "active": true, - "icon": , - }, - ] - - const cloudSyncModal = - { setCloudSyncModalOpen(false) }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - - Enable cloud features - - - What does cloud sync do? -
    - { - setCloudSyncApikey(event.target.value) - }} - /> - -
    - {orgSyncResponse.length > 0 ? - - Error: {orgSyncResponse} - - : null - } - - - {syncList.map((data, index) => { - return ( - - ) - })} - - - * New triggers (userinput, hotmail realtime)
    - * Execute in the cloud rather than onprem
    - * Apps can be built in the cloud
    - * Easily share apps and workflows
    - * Access to powerful cloud search - -
    - - const cancelSubscriptions = (subscription_id) => { - console.log(selectedOrganization) - const orgId = selectedOrganization.id - const data = { - "subscription_id": subscription_id, - "action": "cancel", - "org_id": selectedOrganization.id, - } - - const url = globalUrl + `/api/v1/orgs/${orgId}`; - 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") } + } + } - handleGetOrg(selectedOrganization.id) - return response.json(); - }).then(function(responseJson) { - if (responseJson.success !== undefined && responseJson.success) { - alert.success("Successfully stopped subscription!") + if ( + selectedOrganization.id === undefined && + userdata !== undefined && + userdata.active_org !== undefined && + orgRequest + ) { + setOrgRequest(false); + handleGetOrg(userdata.active_org.id); + } + + const paperStyle = { + maxWidth: 1250, + margin: "auto", + color: "white", + backgroundColor: theme.palette.surfaceColor, + marginBottom: 10, + padding: 20, + }; + + const changeModalData = (field, value) => { + modalUser[field] = value; + }; + + const setUser = (userId, field, value) => { + const data = { user_id: userId }; + data[field] = value; + + fetch(globalUrl + "/api/v1/users/updateuser", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } else { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed setting user: " + responseJson.reason); + } else { + //alert.success("Set the user field " + field + " to " + value); + alert.success("Successfully updated user field " + field) + + if (field !== "suborgs") { + setSelectedUserModalOpen(false); + } + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const generateApikey = (user) => { + const userId = user.id; + const data = { user_id: userId }; + + fetch(globalUrl + "/api/v1/generateapikey", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } else { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed getting new: " + responseJson.reason); + } else { + alert.success("Got new API key"); + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const editAuthenticationModal = selectedAuthenticationModalOpen ? ( + { + setSelectedAuthenticationModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + + Edit authentication for {selectedAuthentication.app.name} ( + {selectedAuthentication.label}) + + + + {selectedAuthentication.fields.map((data, index) => { + //console.log("DATA: ", data, selectedAuthentication) + return ( +
    + + {data.key} + + { + authenticationFields[index].value = e.target.value; + setAuthenticationFields(authenticationFields); + }} + /> +
    + ); + })} +
    + + + + +
    + ) : null; + + const handleOrgEditChange = (event) => { + if (userdata.id === selectedUser.id) { + alert.info("Can't remove orgs from yourself"); + return; + } + + console.log("event: ", event.target.value); + setMatchingOrganizations(event.target.value); + // Workaround for empty orgs + if (event.target.value.length === 0) { + event.target.value.push("REMOVE"); + } + + setUser(selectedUser.id, "suborgs", event.target.value); + //setUser(selectedUser.id, "suborgs", matchingOrganizations) + }; + + const userOrgEdit = + selectedUser.id !== undefined && + selectedUser.orgs !== undefined && + selectedUser.orgs !== null && + selectedOrganization.child_orgs !== undefined && + selectedOrganization.child_orgs !== null && + selectedOrganization.child_orgs.length > 0 ? ( + + + Accessible Sub-Organizations ( + {selectedUser.orgs ? selectedUser.orgs.length - 1 : 0}) + + + + ) : null; + + + + + + const editUserModal = ( + { + setSelectedUserModalOpen(false); + setImage2FA(""); + setSecret2FA(""); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + + Editing {selectedUser.username} + + + + {isCloud ? null : ( +
    + { + setNewUsername(e.target.value); + }} + /> + +
    + )} + + {isCloud ? null : ( +
    + setNewPassword(e.target.value)} + /> + +
    + )} + + {userOrgEdit} + +
    + + + +
    + {show2faSetup ? ( +
    + {/**/} + + {secret2FA !== undefined && + 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. + + + ) : null} + {image2FA !== undefined && + image2FA !== null && + image2FA.length > 0 ? ( + {"2 + ) : ( + + )} + + + After scanning the QR code image, the app will display a code that + you can enter below. + +
    + { + if (event.target.value.length > 6) { + return; + } + + setValue2FA(event.target.value); + }} + /> + +
    +
    + ) : null} +
    +
    + ); + + const GridItem = (props) => { + const [expanded, setExpanded] = React.useState(false); + + const primary = props.data.primary; + const secondary = props.data.secondary; + const primaryIcon = props.data.icon; + const secondaryIcon = props.data.active ? ( + + ) : ( + + ) + + return ( + { + setExpanded(!expanded); + }} + > + + + + {primaryIcon} + + + {secondaryIcon} + + {expanded ? ( +
    + + Usage:  + {props.data.limit === 0 ? ( + "Unlimited" + ) : ( + + {props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit} + + )} + + {/* + Data sharing: {props.data.data_collection} + */} + Description: {secondary} +
    + ) : null} +
    +
    + ); + }; + + const itemColor = "black"; + var syncList = [ + { + primary: "Workflows", + secondary: "", + active: true, + icon: , + }, + { + primary: "Apps", + secondary: "", + active: true, + icon: , + }, + { + primary: "Organization", + secondary: "", + active: true, + icon: , + }, + ]; + + const cloudSyncModal = ( + { + setCloudSyncModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + Enable cloud features + + + What does{" "} + + cloud sync + {" "} + do? +
    + { + setCloudSyncApikey(event.target.value); + }} + /> + +
    + {orgSyncResponse.length > 0 ? ( + + Error: {orgSyncResponse} + + ) : null} + + {syncList.map((data, index) => { + return ; + })} + + * New triggers (userinput, hotmail realtime) +
    + * Execute in the cloud rather than onprem +
    + * Apps can be built in the cloud +
    + * Easily share apps and workflows +
    * Access to powerful cloud search + +
    + ); - } else { - alert.error("Failed stopping subscription. Please contact us.") - } - }) - .catch(function(error) { - console.log("Error: ", error) - alert.error("Failed stopping subscription. Please contact us.") - }) - } + + const organizationView = + curTab === 0 && selectedOrganization.id !== undefined ? ( +
    +
    +

    Organization overview

    + + On this page you can configure individual parts of your + organization.{" "} + + Learn more + + +
    + {selectedOrganization.id === undefined ? ( +
    + + Loading Organization +
    + ) : ( +
    + {/* + + { + console.log("Should go to icon") + }} + > + + + + */} - const organizationView = curTab === 0 && selectedOrganization.id !== undefined ? -
    -
    -

    Organization overview

    - - On this page you can configure individual parts of your organization. Learn more - -
    - {selectedOrganization.id === undefined ? -
    - - - Loading Organization - -
    - : -
    - - { - const elementName = "copy_element_shuffle" - const org_id = selectedOrganization.id - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - const clipboard = navigator.clipboard - if (clipboard === undefined) { - alert.error("Can only copy over HTTPS (port 3443)") - return - } - - navigator.clipboard.writeText(org_id) - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - - alert.info(org_id + " copied to clipboard") - } - }}> - - - - {selectedOrganization.name.length > 0 ? - - : -
    - - - Loading Organization - -
    - } - - Cloud syncronization - What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. - - {isCloud ? -
    -
    - - Currently syncronizing: {selectedOrganization.cloud_sync_active === true ? "True" : "False"} - - {selectedOrganization.cloud_sync_active ? - - Syncronization interval: {selectedOrganization.sync_config.interval === 0 ? "60" : selectedOrganization.sync_config.interval} - - : - null - } - - Your Apikey - - -
    -
    - : -
    -
    - { - setCloudSyncApikey(event.target.value) - }} - /> - -
    - {orgSyncResponse.length > 0 ? - - Message from Shuffle Cloud: {orgSyncResponse} - - : null - } -
    - } - Cloud sync features - - {selectedOrganization.sync_features === undefined || selectedOrganization.sync_features === null ? null : Object.keys(selectedOrganization.sync_features).map(function(key, index) { - if (key === "schedule") { - return null - } + color="primary" + disabled={selectedStatus.length !== 0} + style={{ minWidth: 80, maxWidth: 80, height: "100%", }} + onClick={() => { + console.log("Should send mail to admins of org with context") + handleStatusChange({target: {value: ["contacted"]}}) - const item = selectedOrganization.sync_features[key] - const newkey = key.replaceAll("_", " ") - const griditem = { - "primary": newkey, - "secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description, - "limit": item.limit, - "usage": 0, - "data_collection": "None", - "active": item.active, - "icon": , - } - - return ( - - - - ) - })} - - - {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? -
    - - Your subscription{selectedOrganization.subscriptions.length > 1 ? "s" : ""} - - - {selectedOrganization.subscriptions.reverse().map((sub, index) => { - return ( - - - Type: {sub.level}
    - Recurrence: {sub.recurrence}
    - {sub.active ? -
    - Started: {new Date(sub.startdate*1000).toISOString()}
    - -
    - : -
    - Cancelled: {new Date(sub.cancellationdate*1000).toISOString()}
    - - Status: Deactivated - -
    - } - - - ) - })} - - -
    - : null - } -
    - } - -
    - -
    -
    - : null - - const modalView = - { setModalOpen(false) }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - - {curTab === 1 ? "Add user" : curTab === 6 ? "Add Sub-Organization" : "Add environment"} - - - {curTab === 1 && isCloud ? - - We'll send an email to invite them to your organization. - - : - curTab === 6 ? - - The organization created will become a child of your current organization, and be available to you. - - : - null } - {curTab === 1 ? -
    - Username - changeModalData("Username", event.target.value)} - /> - {isCloud ? null : - - Password - changeModalData("Password", event.target.value)} - /> - - } -
    - : curTab === 6 ? -
    - Name - { - setOrgName(event.target.value) - }} - /> -
    - : curTab === 5 ? -
    - Environment Name - changeModalData("environment", event.target.value)} - /> -
    - : null} - {loginInfo} -
    - - - - -
    - - const usersView = curTab === 1 ? -
    - -
    - - - - - - - - - - - - - - - {users === undefined || users === null ? null : users.map((data, index) => { - var bgColor = "#27292d" - if (index % 2 === 0) { - bgColor = "#1f2023" - } - - return ( - - - - - { - const elementName = "copy_element_shuffle" - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - const clipboard = navigator.clipboard - if (clipboard === undefined) { - alert.error("Can only copy over HTTPS (port 3443)") - return - } - - navigator.clipboard.writeText(data.apikey) - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - - alert.info("Apikey copied to clipboard") - } - }}> - - - - }/> - - { - console.log("VALUE: ", e.target.value) - setUser(data.id, "role", e.target.value) - }} - style={{ backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px" }} + // open a mailto with subject "hello" and sender "frikky@shuffler.io" + }} > - - Admin - - - User - - - } - style ={{ minWidth: 135, maxWidth: 135, marginRight: 15,}} + Sales mail + + + + Status + + + + : null} + + + { + const elementName = "copy_element_shuffle"; + const org_id = selectedOrganization.id; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(org_id); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info(org_id + " copied to clipboard"); + } + }} + > + + + + {selectedOrganization.defaults !== undefined && selectedOrganization.defaults.documentation_reference !== undefined && selectedOrganization.defaults.documentation_reference !== null && selectedOrganization.defaults.documentation_reference.includes("http") ? + + + + + + + + : null} + {selectedOrganization.name.length > 0 ? ( + + ) : ( +
    + + Loading Organization +
    + )} + + { + const newValue = parseInt(inputValue); + setAdminTab(newValue); + + //const setConfig = (event, inputValue) => { + navigate(`/admin?admin_tab=${admin_views[newValue]}`); + }} + aria-label="disabled tabs example" + > + + Edit Details + /> - - - - { - setSelectedUserModalOpen(true) - setSelectedUser(data) - }} - > - - - + ) : null} +
    +
    +
    + ) : ( +
    +
    + { + setCloudSyncApikey(event.target.value); + }} + /> + +
    + {orgSyncResponse.length > 0 ? ( + + Message from Shuffle Cloud: {orgSyncResponse} + + ) : null} +
    + )} + + Cloud sync features + + + If not otherwise specified, Usage will reset monthly + + + + {selectedOrganization.sync_features === undefined || + selectedOrganization.sync_features === null + ? null + : Object.keys(selectedOrganization.sync_features).map(function ( + key, + index + ) { + // unnecessary parts + if (key === "schedule" || key === "apps" || key === "updates") { + return null; + } + + const item = selectedOrganization.sync_features[key]; + if (item === null) { + return null + } + + const newkey = key.replaceAll("_", " "); + const griditem = { + primary: newkey, + secondary: + item.description === undefined || + item.description === null || + item.description.length === 0 + ? "Not defined yet" + : item.description, + limit: item.limit, + usage: item.usage === undefined || + item.usage === null ? 0 : item.usage, + data_collection: "None", + active: item.active, + icon: , + }; + + return ( + + + + ); + })} + +
    + ) + : adminTab === 2 ? + + : adminTab === 3 ? + + : adminTab === 4 ? + + : null + } + + + +
    + )} +
    + ) : null; + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + + {curTab === 1 ? "Add user" : curTab === 7 ? "Add Sub-Organization" : "Add environment"} + + + + {curTab === 1 && isCloud ? ( + + We will send an email to invite them to your organization. + + ) : curTab === 7 ? ( + + The organization created will become a child of your current + organization, and be available to you. + + ) : null} + {curTab === 1 ? ( +
    + Username + + changeModalData("Username", event.target.value) + } + /> + {isCloud ? null : ( + + Password + + changeModalData("Password", event.target.value) + } + /> + + )} +
    + ) : curTab === 7 ? ( +
    + Name + { + setOrgName(event.target.value); + }} + /> +
    + ) : curTab === 6 ? ( +
    + Environment Name + + changeModalData("environment", event.target.value) + } + /> +
    + ) : null} + {loginInfo} +
    + + + + +
    + ); + + const usersView = + curTab === 1 ? ( +
    +
    +

    User management

    + + Add, edit, block or change passwords.{" "} + + Learn more + + +
    +
    + + + + + + + + + + + + + + {selectedOrganization.child_orgs !== undefined && + selectedOrganization.child_orgs !== null && + selectedOrganization.child_orgs.length > 0 ? ( + + ) : null} + + + {users === undefined || users === null + ? null + : users.map((data, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + { + const elementName = "copy_element_shuffle"; + var copyText = + document.getElementById(elementName); + if ( + copyText !== null && + copyText !== undefined + ) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error( + "Can only copy over HTTPS (port 3443)" + ); + return; + } + + navigator.clipboard.writeText(data.apikey); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info("Apikey copied to clipboard"); + } + }} + > + + + + ) + } + /> + + { + console.log("VALUE: ", e.target.value); + setUser(data.id, "role", e.target.value); + }} + style={{ + backgroundColor: theme.palette.surfaceColor, + color: "white", + height: "50px", + }} + > + + Org Admin + + + Org User + + + Org Reader + + + } + style={{ minWidth: 135, maxWidth: 135, marginRight: 15 }} + /> + + + + {selectedOrganization.child_orgs !== undefined && + selectedOrganization.child_orgs !== null && + selectedOrganization.child_orgs.length > 0 ? ( + + ) : null} + + { + setSelectedUserModalOpen(true); + setSelectedUser(data); + + // Find matching orgs between current org and current user's access to those orgs + if ( + userdata.orgs !== undefined && + userdata.orgs !== null && + userdata.orgs.length > 0 && + selectedOrganization.child_orgs !== undefined && + selectedOrganization.child_orgs !== null && + selectedOrganization.child_orgs.length > 0 + ) { + console.log("In here?"); + var active = []; + for (var key in userdata.orgs) { + console.log("ORG: ", userdata.orgs[key]); + const found = + selectedOrganization.child_orgs.find( + (item) => item.id === userdata.orgs[key].id + ); + if (found !== null && found !== undefined) { + if ( + data.orgs === undefined || + data.orgs === null + ) { + continue; + } + + const subfound = data.orgs.find( + (item) => item === found.id + ); + if ( + subfound !== null && + subfound !== undefined + ) { + active.push(subfound); + } + } + } + + setMatchingOrganizations(active); + } + }} + > + + + {/* - - - ) - })} - -
    - : null + */} + + + ); + })} + +
    + ) : null; - const uploadFiles = (files) => { - for (var key in files) { - try { - const filename = files[key].name - var filedata = new FormData() - filedata.append('shuffle_file', files[key]) + const run2FASetup = (data) => { + console.log("2fa: ", data, show2faSetup); + if (!show2faSetup) { + get2faCode(data.id); + } else { + // Should remove? + setImage2FA(""); + setSecret2FA(""); + } - if (typeof(files[key]) === "object") { - handleCreateFile(filename, filedata) - } + setShow2faSetup(!show2faSetup); + //setShow2faSetup(true); + }; - /* - reader.addEventListener('load', (e) => { - var data = e.target.result; - setIsDropzone(false) - console.log(filename) - console.log(data) - console.log(files[key]) - }) - reader.readAsText(files[key]) - */ - } catch (e) { - console.log("Error in dropzone: ", e) - } - } - setTimeout(() => { - getFiles() - }, 2500) + + const filesView = curTab !== 3 ? null : + + + const schedulesView = + curTab === 5 ? ( +
    +
    +

    Schedules

    + + Schedules used in Workflows. Makes locating and control easier.{" "} + + Learn more + + +
    + + + + + + + + + + {schedules === undefined || schedules === null + ? null + : schedules.map((schedule, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + {schedule.seconds} seconds + ) + } + /> + + + {schedule.workflow_id} + + } + /> + + + + + + ); + })} + +
    + ) : null; + + const appCategoryView = + curTab === 8 ? ( +
    +
    +

    Categories

    + + Categories are the categories supported by Shuffle, which are mapped + to apps and workflows + +
    + + + + + + + + + + {categories.map((data, index) => { + if (data.apps.length === 0) { + return null; + } + + return ( + + + + + + + + + + ); + })} + +
    + ) : null; + + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true); + setSelectedAuthentication(field); + //{selectedAuthentication.fields.map((data, index) => { + var newfields = []; + for (var key in field.fields) { + newfields.push({ + key: field.fields[key].key, + value: "", + }); + } + setAuthenticationFields(newfields); + }; + + const authenticationView = + curTab === 2 ? ( +
    +
    +

    App Authentication

    + + Control the authentication options for individual apps. + +   + + Learn more about App Authentication + +
    + + + + + + + {/**/} + + {/* + + */} + + + + + {authentication === undefined || authentication === null + ? null + : authentication.map((data, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + //console.log("Auth data: ", data) + if (data.type === "oauth2") { + data.fields = [ + { + key: "url", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_id", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_secret", + value: "Secret. Replaced during app execution!", + }, + { + key: "scope", + value: "Secret. Replaced during app execution!", + }, + ]; + } + + return ( + + + style={{ minWidth: 75, maxWidth: 75 }} + /> + + + {/* + + */} + + {/* + + */} + { + return data.key; + }) + .join(", ") + } + style={{ + minWidth: 125, + maxWidth: 125, + overflow: "hidden", + }} + /> + + + { + updateAppAuthentication(data); + }} + > + + + {data.defined ? ( + + { + editAuthenticationConfig(data.id); + }} + > + + + + ) : ( + + {}} + > + + + + )} + { + deleteAuthentication(data); + }} + > + + + + + ); + })} + +
    + ) : null; + + const changeRecommendation = (recommendation, action) => { + const data = { + action: action, + name: recommendation.name, + }; + + fetch(`${globalUrl}/api/v1/recommendations/modify`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (checkLogin !== undefined) { + checkLogin() + getEnvironments() + } + } else { + if (responseJson.success === false && responseJson.reason !== undefined) { + alert.error("Failed change recommendation: ", responseJson.reason) + } else { + alert.error("Failed change recommendation"); + } + } + }) + .catch((error) => { + alert.info("Failed dismissing alert. Please contact support@shuffler.io if this persists."); + }); } - const uploadFile = (e) => { - const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; - const files = isDropzone ? e.dataTransfer.files : e.target.files; - - //const reader = new FileReader(); - //alert.info("Starting fileupload") - uploadFiles(files) - } + const environmentView = + curTab === 6 ? ( +
    +
    +

    Environments

    + + Decides what Orborus environment to execute an action in a workflow + in.{" "} + + Learn more + + +
    + + + { + setShowArchived(!showArchived); + }} + />{" "} + Show disabled + + + + + + + + + + + + + {environments === undefined || environments === null + ? null + : environments.map((environment, index) => { + if (!showArchived && environment.archived) { + return null; + } - const filesView = curTab === 3 ? - 1366 ? 1366 : 1200, margin: "auto", padding: 20 }} onDrop={uploadFile}> -
    -
    -

    Files

    - Files from Workflows. Learn more -
    - - upload = ref} onChange={(event) => { - //const file = event.target.value - //const fileObject = URL.createObjectURL(actualFile) - //setFile(fileObject) - //const files = event.target.files[0] - uploadFiles(event.target.files) - }} /> - + if (environment.archived === undefined) { + return null; + } - {fileNamespaces !== undefined && fileNamespaces !== null && fileNamespaces.length > 1 ? - - Namespace - - - : null} + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } - - - - - - - - - - - - - {files === undefined || files === null || files.length === 0 ? null : files.map((file, index) => { - if (file.namespace === "") { - file.namespace = "default" - } + // Check if there's a notification for it in userdata.priorities + var showCPUAlert = false + var foundIndex = -1 + if (userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) { + foundIndex = userdata.priorities.findIndex(prio => prio.name.includes("CPU") && prio.active === true) - if (file.namespace !== selectedNamespace) { - return null - } + if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) { + showCPUAlert = true + } + } - var bgColor = "#27292d" - if (index % 2 === 0) { - bgColor = "#1f2023" - } + console.log("Show CPU alert: ", showCPUAlert) - return ( - - - - - - - : - - - - - + return ( + + + + + Not running +
    + : environment.running_ip.split(":")[0] + : "N/A" + } + style={{ + minWidth: 200, + maxWidth: 200, + overflow: "hidden", + }} + /> + + + { + if (environment.Type === "cloud") { + alert.info("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.") + return + } + + const elementName = "copy_element_shuffle"; + const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth + const commandData = `docker run --volume "/var/run/docker.sock:/var/run/docker.sock" -e ENVIRONMENT_NAME="${environment.Name}" -e 'AUTH=${auth}' -e ORG="${props.userdata.active_org.id}" -e DOCKER_API_VERSION=1.40 -e BASE_URL="${globalUrl}" --name="shuffle-orborus" -d ghcr.io/shuffle/shuffle-orborus:latest` + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(commandData); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info("Orborus command copied to clipboard"); + } + }} + > + - - - + + } + /> + + + + {environment.default ? null : ( + + )} + + + + +
    + + + + +
    +
    + + {showCPUAlert === false ? null : + +
    +
    + + 90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found. + + + Need help with High Availability and Scale? Read documentation and Get in touch. + +
    +
    + +
    +
    +
    } - style={{minWidth: 100, maxWidth: 100, overflow: "hidden"}} - /> - - - - - - { - downloadFile(file) - }}> - - - - - style={{minWidth: 75, maxWidth: 75, overflow: "hidden"}} - /> - {/* - - - - */} - { - const elementName = "copy_element_shuffle" - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - const clipboard = navigator.clipboard - if (clipboard === undefined) { - alert.error("Can only copy over HTTPS (port 3443)") - return - } + + ); + })} +
    + {/**/} +
    + ) : null; - navigator.clipboard.writeText(file.id) - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ + const organizationsTab = + curTab === 7 ? ( +
    +
    +

    Organizations

    + + Global admin: control organizations + +
    + + + + + + + + + + + + {userdata.orgs !== undefined && + userdata.orgs !== null && + userdata.orgs.length > 0 ? ( + + {userdata.orgs.map((data, index) => { + const isSelected = + props.userdata.active_org.id === undefined + ? "False" + : props.userdata.active_org.id === data.id + ? "True" + : "False"; - /* Copy the text inside the text field */ - document.execCommand("copy"); + const imagesize = 40; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + }; + const image = + data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); - alert.info(file.id + " copied to clipboard") - } - }}> - - - /> - - ) - })} - -
    - - : null + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } - const schedulesView = curTab === 4 ? -
    -
    -

    Schedules

    - Schedules used in Workflows. Makes locating and control easier. Learn more -
    - - - - - - - - - - {schedules === undefined || schedules === null ? null : schedules.map((schedule, index) => { - var bgColor = "#27292d" - if (index % 2 === 0) { - bgColor = "#1f2023" - } + return ( + + + + + + + { + setCloudSyncModalOpen(true); + setSelectedOrganization(data); + console.log("INVERT CLOUD SYNC"); + }} + /> + style={{ minWidth: 150, maxWidth: 150 }} + /> + + ); + })} + + ) : null} + +
    + ) : null; - return ( - - {schedule.seconds} seconds} - /> - - {schedule.workflow_id}} - /> - - - - - - ) - })} - -
    - : null + const hybridTab = + curTab === 8 ? ( +
    +
    +

    Hybrid

    + +
    + + + + + + + + + + + { + console.log("INVERT"); + }} + /> + style={{ minWidth: 150, maxWidth: 150 }} + /> + + +
    + ) : null; - const appCategoryView = curTab === 7 ? -
    -
    -

    Categories

    - - Categories are the categories supported by Shuffle, which are mapped to apps and workflows - -
    - - - - - - - - - - {categories.map((data, index) => { - if (data.apps.length === 0) { - return null - } + const cacheOrgView = + curTab === 4 ? ( +
    + +
    + ) : null; - return ( - - - - - - - - - - ) - })} -
    -
    - : null + // primary={environment.Registered ? "true" : "false"} - const updateAppAuthentication = (field) => { - setSelectedAuthenticationModalOpen(true) - setSelectedAuthentication(field) - //{selectedAuthentication.fields.map((data, index) => { - var newfields = [] - for (var key in field.fields) { - newfields.push({ - "key": field.fields[key].key, - "value": "", - }) - } - setAuthenticationFields(newfields) - } + const iconStyle = { marginRight: 10 }; + const data = ( +
    + + + + Organization + + /> + + + Users + + /> + + + App Authentication + + /> + + + Files + + /> + + Datastore + + /> + + + Schedules + + /> + + + Environments + + /> + + Tenants + + /> - const authenticationView = curTab === 2 ? -
    -
    -

    App Authentication

    - Control the authentication options for individual apps. Actions can be destructive! -  Learn more -
    - - - - - - - - - - - - - {authentication === undefined || authentication === null ? null : authentication.map((data, index) => { - var bgColor = "#27292d" - if (index % 2 === 0) { - bgColor = "#1f2023" - } + {/*window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null*/} + {/*window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null*/} + + +
    + {organizationView} + {authenticationView} + {usersView} + {environmentView} + {schedulesView} + {filesView} + {hybridTab} + {organizationsTab} + {appCategoryView} + {cacheOrgView} +
    + +
    + ); + return ( +
    + {modalView} + {cloudSyncModal} + {editUserModal} + {editAuthenticationModal} + {data} + +
    + ); +}; - return ( - - - style={{minWidth: 75, maxWidth: 75}} - /> - - - - - - {return data.key}).join(", ")} - style={{minWidth: 200, maxWidth: 200, overflow: "hidden"}} - /> - - { - updateAppAuthentication(data) - }} - > - - - {data.defined ? - - { - editAuthenticationConfig(data.id) - }} - > - - - - : - - { - }} - > - - - - } - { - deleteAuthentication(data) - }} - > - - - - - ) - })} - -
    - : null - - const environmentView = curTab === 5 ? -
    -
    -

    Environments

    - Decides what Orborus environment to execute an action in a workflow in. Learn more -
    - - - {setShowArchived(!showArchived)}} /> Show archived - - - - - - - - - - - {environments === undefined || environments === null ? null : environments.map((environment, index)=> { - if (!showArchived && environment.archived) { - return null - } - - if (environment.archived === undefined) { - getEnvironments() - return null - } - - //var bgColor = "#27292d" - //if (index % 2 === 0) { - // bgColor = "#1f2023" - //} - - return ( - - - - - - {environment.default ? - null - : - - } - - - - {/**/} - - - - ) - })} - -
    - : null - - const organizationsTab = curTab === 6 ? -
    -
    -

    Organizations

    - Global admin: control organizations -
    - - - - - - - - - - - - {userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? - - {userdata.orgs.map((data, index) => { - const isSelected = props.userdata.active_org.id === undefined ? "False" : props.userdata.active_org.id === data.id ? "True" : "False" - - const imagesize = 40 - const imageStyle = {width: imagesize, height: imagesize, pointerEvents: "none", } - const image = data.image === "" ? - {data.name} - : - {data.name} - - var bgColor = "#27292d" - if (index % 2 === 0) { - bgColor = "#1f2023" - } - - return ( - - - - - - - { - setCloudSyncModalOpen(true) - setSelectedOrganization(data) - console.log("INVERT CLOUD SYNC") - }} /> - style={{minWidth: 150, maxWidth: 150}} - /> - - ) - })} - - : null} - -
    - : null - - const hybridTab = curTab === 7 ? -
    -
    -

    Hybrid

    - -
    - - - - - - - - - - - {console.log("INVERT")}} /> - style={{minWidth: 150, maxWidth: 150}} - /> - - -
    - : null - - // primary={environment.Registered ? "true" : "false"} - - - - const iconStyle = {marginRight: 10} - const data = -
    - - - Organization/> - Users /> - App Authentication/> - Files /> - Schedules /> - {isCloud ? null : Environments/>} - {isCloud ? null : Organizations/>} - {/*window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null*/} - {/*window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null*/} - - -
    - {organizationView} - {authenticationView} - {usersView} - {environmentView} - {schedulesView} - {filesView} - {hybridTab} - {organizationsTab} - {appCategoryView} -
    -
    -
    - - return ( -
    - {modalView} - {cloudSyncModal} - {editUserModal} - {editAuthenticationModal} - {data} - -
    - ) -} - -export default Admin +export default Admin; diff --git a/frontend/src/views/AdminSetup.jsx b/frontend/src/views/AdminSetup.jsx old mode 100644 new mode 100755 diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx old mode 100644 new mode 100755 index 4d5beeb1..36c82003 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1,42 +1,237 @@ -import React, {useRef, useState, useEffect, useLayoutEffect} from 'react'; -import { useInterval } from 'react-powerhooks'; -import { useTheme } from '@material-ui/core/styles'; +/* eslint-disable react/no-multi-comp */ +import React, { useState, useEffect, useLayoutEffect } from "react"; +import ReactDOM from "react-dom" -import { v4 as uuidv4 } from 'uuid'; -import {Link} from 'react-router-dom'; -import { Prompt } from 'react-router' -import { useBeforeunload } from 'react-beforeunload'; -import ReactJson from 'react-json-view' +import { useInterval } from "react-powerhooks"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; + +import { v4 as uuidv4 } from "uuid"; +import { useNavigate, Link, useParams } from "react-router-dom"; +// import { Prompt } from "react-router"; // FIXME +import { useBeforeunload } from "react-beforeunload"; +import ReactJson from "react-json-view"; import NestedMenuItem from "material-ui-nested-menu-item"; -import ReactMarkdown from 'react-markdown'; - - -import {TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade} from '@material-ui/core'; -import {OpenInNew as OpenInNewIcon,Undo as UndoIcon, FileCopy as FileCopyIcon, GetApp as GetAppIcon, Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; - -import * as cytoscape from 'cytoscape'; -import * as edgehandles from 'cytoscape-edgehandles'; -import * as clipboard from 'cytoscape-clipboard'; -import CytoscapeComponent from 'react-cytoscapejs'; -import undoRedo from 'cytoscape-undo-redo'; -import Draggable from 'react-draggable'; - -import cytoscapestyle from '../defaultCytoscapeStyle'; -import cxtmenu from 'cytoscape-cxtmenu'; - -import { w3cwebsocket as W3CWebSocket } from "websocket"; +import ReactMarkdown from "react-markdown"; import { useAlert } from "react-alert"; +import theme from '../theme.jsx'; +import { isMobile } from "react-device-detect" +import aa from 'search-insights' +import Drift from "react-driftjs"; + +//import { Cron } from 'react-js-cron'; +//import 'react-js-cron/dist/styles.css' + + +import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; +import algoliasearch from 'algoliasearch/lite'; + +import { + Zoom, + Avatar, + Popover, + TextField, + Drawer, + Button, + Paper, + Grid, + Tabs, + InputAdornment, + Tab, + ButtonBase, + Tooltip, + Select, + MenuItem, + Divider, + Dialog, + DialogActions, + DialogTitle, + InputLabel, + DialogContent, + FormControl, + IconButton, + Menu, + Input, + Fade, + FormGroup, + FormControlLabel, + Typography, + Checkbox, + Breadcrumbs, + CircularProgress, + SwipeableDrawer, + Switch, + Chip, + Card, + List, + ListItem, + ListItemText, + ListItemAvatar, + Badge, +} from "@material-ui/core"; + +import { + AvatarGroup, +} from "@mui/material" + +import { + Folder as FolderIcon, + LibraryBooks as LibraryBooksIcon, + OpenInNew as OpenInNewIcon, + Undo as UndoIcon, + GetApp as GetAppIcon, + Search as SearchIcon, + Visibility as VisibilityIcon, + Done as DoneIcon, + Close as CloseIcon, + Error as ErrorIcon, + ArrowLeft as ArrowLeftIcon, + Cached as CachedIcon, + DirectionsRun as DirectionsRunIcon, + Polymer as PolymerIcon, + FormatListNumbered as FormatListNumberedIcon, + PlayArrow as PlayArrowIcon, + AspectRatio as AspectRatioIcon, + MoreVert as MoreVertIcon, + Apps as AppsIcon, + Schedule as ScheduleIcon, + FavoriteBorder as FavoriteBorderIcon, + Pause as PauseIcon, + Delete as DeleteIcon, + AddCircleOutline as AddCircleOutlineIcon, + Save as SaveIcon, + KeyboardArrowRight as KeyboardArrowRightIcon, + ArrowBack as ArrowBackIcon, + Settings as SettingsIcon, + LockOpen as LockOpenIcon, + ExpandMore as ExpandMoreIcon, + VpnKey as VpnKeyIcon, + AddComment as AddCommentIcon, + Edit as EditIcon, + Send as SendIcon, +} from "@material-ui/icons"; + +import { + Preview as PreviewIcon, + ContentCopy as ContentCopyIcon, + Circle as CircleIcon, + SquareFoot as SquareFootIcon, + AutoFixHigh as AutoFixHighIcon, +} from '@mui/icons-material'; + +import Autocomplete from "@material-ui/lab/Autocomplete"; + +import * as cytoscape from "cytoscape"; +import * as edgehandles from "cytoscape-edgehandles"; +import * as clipboard from "cytoscape-clipboard"; +import CytoscapeComponent from "react-cytoscapejs"; +import undoRedo from "cytoscape-undo-redo"; + +import Draggable from "react-draggable"; + +import cytoscapestyle from "../defaultCytoscapeStyle.jsx"; +import cxtmenu from "cytoscape-cxtmenu"; + import { validateJson, GetIconInfo } from "./Workflows.jsx"; import { GetParsedPaths } from "./Apps.jsx"; -import ConfigureWorkflow from '../components/ConfigureWorkflow.jsx'; +import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; -import ParsedAction from '../components/ParsedAction.jsx'; -//import Scroll from 'react-scroll' -//import { Element as ScrollElement, animateScroll as scroll, scrollSpy, scroller } from 'react-scroll' +import ParsedAction from "../components/ParsedAction.jsx"; +import PaperComponent from "../components/PaperComponent.jsx" +import ExtraApps from "../components/ExtraApps.jsx" +import EditWorkflow from "../components/EditWorkflow.jsx" +// import AppStats from "../components/AppStats.jsx"; -const surfaceColor = "#27292D" -const inputColor = "#383B40" -const appSorter = "favorite_apps" +export const triggers = [ + { + name: "Webhook", + type: "TRIGGER", + status: "uninitialized", + trigger_type: "WEBHOOK", + errors: null, + large_image: + "data:image/png;base64,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", + is_valid: true, + label: "Webhook", + environment: "onprem", + description: "Custom HTTP input", + long_description: "Execute a workflow with an unauthicated POST request", + }, + { + name: "Schedule", + type: "TRIGGER", + status: "uninitialized", + description: "Specify time", + trigger_type: "SCHEDULE", + errors: null, + large_image: + "data:image/png;base64,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", + label: "Schedule", + is_valid: true, + environment: "onprem", + long_description: "Create a schedule based on cron", + }, + + { + name: "Office365", + type: "TRIGGER", + status: "uninitialized", + description: "O365 email trigger", + trigger_type: "EMAIL", + errors: null, + is_valid: true, + label: "Email", + environment: "cloud", + large_image: + "data:image/jpg;base64,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", + long_description: "Execute a workflow when you get an email", + }, + { + name: "Gmail", + type: "TRIGGER", + status: "uninitialized", + description: "Gmail email trigger", + trigger_type: "EMAIL", + errors: null, + is_valid: true, + label: "Email", + environment: "cloud", + large_image: + "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAzFodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QTIyMjgyMEYwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QTIyMjgyMTAwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozQTMwMDQxRTAwMEUxMUVCQkE4QTk5QkEzQzMxMDZEMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBMjIyODIwRTAwMkMxMUVCQkE4QTk5QkEzQzMxMDZEMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAwICAwMCAwMDAwMEBwUEBAQECQYHBQcKCQsL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", + long_description: "Execute a workflow when you get an email", + }, + { + name: "Shuffle Workflow", + type: "TRIGGER", + status: "uninitialized", + trigger_type: "SUBFLOW", + errors: null, + large_image: + "data:image/png;base64,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", + is_valid: true, + label: "Subflow", + environment: "onprem", + description: "Control a workflow", + long_description: "Execute another workflow from this workflow", + }, + { + name: "User Input", + type: "TRIGGER", + status: "running", + large_image: + "data:image/png;base64,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", + description: "Wait for user input", + trigger_type: "USERINPUT", + is_valid: true, + errors: null, + label: "User input", + environment: "cloud", + long_description: "Take user input to continue execution", + }, + ]; + // is_valid: cloudSyncEnabled || isCloud ? true : false, + +const surfaceColor = "#27292D"; +const inputColor = "#383B40"; // http://apps.cytoscape.org/apps/yfileslayoutalgorithms cytoscape.use(edgehandles); @@ -48,9405 +243,16232 @@ cytoscape.use(cxtmenu); //import popper from 'cytoscape-popper'; //cytoscape.use(popper); - // https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react function useWindowSize() { - const [size, setSize] = useState([0, 0]); - useLayoutEffect(() => { - function updateSize() { - setSize([window.innerWidth, window.innerHeight]); - } - window.addEventListener('resize', updateSize); - updateSize(); - return () => window.removeEventListener('resize', updateSize); - }, []); - return size; + const [size, setSize] = useState([0, 0]); + useLayoutEffect(() => { + function updateSize() { + setSize([window.innerWidth, window.innerHeight]); + } + window.addEventListener("resize", updateSize); + updateSize(); + return () => window.removeEventListener("resize", updateSize); + }, []); + return size; } export function sortByKey(array, key) { - if (array === undefined) { - return [] - } + if (array === undefined) { + return []; + } - if (key.startsWith("-") && key.length > 2) { - key = key.slice(1, key.length) - return array.sort(function(a, b) { - var x = a[key]; var y = b[key] - return ((x < y) ? -1 : ((x > y) ? 1 : 0)) - }).reverse() - } + if (key.startsWith("-") && key.length > 2) { + key = key.slice(1, key.length); + return array + .sort(function (a, b) { + var x = a[key]; + var y = b[key]; + return x < y ? -1 : x > y ? 1 : 0; + }) + .reverse(); + } - if (array === undefined || array === null) { - return [] - } + if (array === undefined || array === null) { + return []; + } - return array.sort(function(a, b) { - var x = a[key]; var y = b[key] - return ((x < y) ? -1 : ((x > y) ? 1 : 0)) - }) -} - -function removeParam(key, sourceURL) { - if (sourceURL === undefined) { - return - } - - var rtn = sourceURL.split("?")[0], - param, - params_arr = [], - queryString = (sourceURL.indexOf("?") !== -1) ? sourceURL.split("?")[1] : ""; - - if (queryString !== "") { - params_arr = queryString.split("&"); - for (var i = params_arr.length - 1; i >= 0; i -= 1) { - param = params_arr[i].split("=")[0]; - if (param === key) { - params_arr.splice(i, 1); - } - } - rtn = rtn + "?" + params_arr.join("&"); - } - - if (rtn === "?") { - return "" - } - - return rtn; -} - -const splitter = "|~|" -const svgSize = 24 -//const referenceUrl = "https://shuffler.io/functions/webhooks/" -//const referenceUrl = window.location.origin+"/api/v1/hooks/" - -function useTraceUpdate(props) { - const prev = useRef(props) - useEffect(() => { - const changedProps = Object.entries(props).reduce((ps, [k, v]) => { - if (prev.current[k] !== v) { - ps[k] = [prev.current[k], v]; - } - return ps; - }, {}); - if (Object.keys(changedProps).length > 0) { - console.log('Changed props:', changedProps); - } - prev.current = props; + return array.sort(function (a, b) { + var x = a[key]; + var y = b[key]; + return x < y ? -1 : x > y ? 1 : 0; }); } -const AngularWorkflow = (props) => { - const { globalUrl, isLoggedIn, isLoaded, userdata } = props; - const referenceUrl = globalUrl+"/api/v1/hooks/" - const alert = useAlert() - const theme = useTheme(); - const green = "#86c142" - const yellow = "#FECC00" +// look for keys and set values with shuffle dotnotation +// used primarily for AI autocompletions +export function SetJsonDotnotation(jsonInput, inputKey) { - const [bodyWidth, bodyHeight] = useWindowSize(); + if (jsonInput === undefined || jsonInput === null) { + return jsonInput; + } - var to_be_copied = "" - const [cystyle, ] = useState(cytoscapestyle) - const [cy, setCy] = React.useState() - - const [currentView, setCurrentView] = React.useState(0) - const [triggerAuthentication, setTriggerAuthentication] = React.useState({}) - const [triggerFolders, setTriggerFolders] = React.useState([]) - const [workflows, setWorkflows] = React.useState([]) - const [showEnvironment, setShowEnvironment] = React.useState(false) + // Check for array + if (Array.isArray(jsonInput)) { + for (var i = 0; i < jsonInput.length; i++) { + jsonInput[i] = SetJsonDotnotation(jsonInput[i], inputKey+".#"); + } - const [workflow, setWorkflow] = React.useState({}); - const [userSettings, setUserSettings] = React.useState({}); - const [subworkflow, setSubworkflow] = React.useState({}); - const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); - const [leftViewOpen, setLeftViewOpen] = React.useState(true); - const [leftBarSize, setLeftBarSize] = React.useState(350) - const [executionText, setExecutionText] = React.useState(""); - const [executionRequestStarted, setExecutionRequestStarted] = React.useState(false); - const [scrollConfig, setScrollConfig] = React.useState({ - top: 0, - left: 0, - selected: "", - }) - - const [history, setHistory] = React.useState([]) - const [historyIndex, setHistoryIndex] = React.useState(history.length) - - const [appAuthentication, setAppAuthentication] = React.useState([]); - const [variablesModalOpen, setVariablesModalOpen] = React.useState(false); - const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = React.useState(false); - const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); - const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false); - const [newVariableName, setNewVariableName] = React.useState(""); - const [authenticationType, setAuthenticationType] = React.useState(""); - const [newVariableDescription, setNewVariableDescription] = React.useState(""); - const [newVariableValue, setNewVariableValue] = React.useState(""); - const [workflowDone, setWorkflowDone] = React.useState(false) - const [authLoaded, setAuthLoaded] = React.useState(false) - const [localFirstrequest, setLocalFirstrequest] = React.useState(true) - const [requiresAuthentication, setRequiresAuthentication] = React.useState(false) - const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) - const [showSkippedActions, setShowSkippedActions] = React.useState(false) - const [lastExecution, setLastExecution] = React.useState("") - const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false) - const [curpath, setCurpath] = useState(typeof window === 'undefined' || window.location === undefined ? "" : window.location.pathname) - - // 0 = normal, 1 = just done, 2 = normal - const [savingState, setSavingState] = React.useState(0) - - const [selectedResult, setSelectedResult] = React.useState({}) - const [codeModalOpen, setCodeModalOpen] = React.useState(false); - - const [variableAnchorEl, setVariableAnchorEl] = React.useState(null) - - const [sourceValue, setSourceValue] = React.useState({}) - const [destinationValue, setDestinationValue] = React.useState({}) - const [conditionValue, setConditionValue] = React.useState({}) - const [dragging, setDragging] = React.useState(false) - const [dragPosition, setDragPosition] = React.useState({ - x: 0, - y: 0, - }) - - // Trigger stuff - const [selectedTrigger, setSelectedTrigger] = React.useState({}); - const [selectedTriggerIndex, setSelectedTriggerIndex] = React.useState({}); - const [selectedEdge, setSelectedEdge] = React.useState({}); - const [selectedEdgeIndex, setSelectedEdgeIndex] = React.useState({}); - - - const [visited, setVisited] = React.useState([]); - //const [workflow, setWorkflow] = React.useState(workflowdata); + return jsonInput; + // Check for dict + } else if (typeof jsonInput === "object") { + // Loop keys and values - const [apps, setApps] = React.useState([]); - const [filteredApps, setFilteredApps] = React.useState([]); - const [prioritizedApps, setPrioritizedApps] = React.useState([]); - const [firstrequest, setFirstrequest] = React.useState(true) - //const [apps, setApps] = React.useState(appdata); - //const [filteredApps, setFilteredApps] = React.useState(); - - const [environments, setEnvironments] = React.useState([]); - const [established, setEstablished] = React.useState(false); - - const [graphSetup, setGraphSetup] = React.useState(false); - - const [selectedApp, setSelectedApp] = React.useState({}); - const [selectedAction, setSelectedAction] = React.useState({}); - const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); - - const [executionRequest, setExecutionRequest] = React.useState({}) - - const [, setExecutingNodes] = React.useState([]) - const [executionRunning, setExecutionRunning] = React.useState(false) - const [executionModalOpen, setExecutionModalOpen] = React.useState(false) - const [executionModalView, setExecutionModalView] = React.useState(0) - const [executionData, setExecutionData] = React.useState({}) - - const [lastSaved, setLastSaved] = React.useState(true) - - const [appAdded, setAppAdded] = useState(false) - const [update, setUpdate] = useState(""); - const [workflowExecutions, setWorkflowExecutions] = React.useState([]); - const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0) - - // This should all be set once, not on every iteration - // Use states and don't update lol - const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false - //const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" - const appBarSize = isCloud ? 75 : 60 - const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"] - const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' - - useBeforeunload(() => { - if (!lastSaved) { - return unloadText - } - }) - - const [elements, setElements] = useState([]) - // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. - // Set it from 2500 to 6000 to reduce overall load - const { start, stop } = useInterval({ - duration: 3000, - startImmediate: false, - callback: () => { - fetchUpdates() - } - }) - - 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) - - // Sets up subflow trigger with the right info - if (trigger_index > -1) { - var outersub = {} - const trigger = workflow.triggers[trigger_index] - if (trigger.parameters.length >= 3) { - for (var key in trigger.parameters) { - const param = trigger.parameters[key] - if (param.name === "workflow") { - if (param.value === workflow.id) { - setSubworkflow(workflow) - outersub = workflow - } else { - const sub = responseJson.find(data => data.id === param.value) - if (sub !== undefined && subworkflow.id !== sub.id) { - setSubworkflow(sub) - outersub = sub - } - } - } - - console.log("PARAM: ", param) - //console.log("PARAMVAL: ", param.value) - if (param.name === "startnode" && param.value !== undefined && param.value !== null) { - //console.log("SHOULD SET STARTNODE IN SUBFLOW SELECTION: ", outersub) - //const innernode = outersub.actions.find(action => action.id === param.value) - //console.log("FOUND NODE: ", innernode) - //if (innernode !== undefined && subworkflowStartnode.id !== innernode.id) { - //setSubworkflowStartnode(JSON.parse(JSON.stringify(param.value))) - setSubworkflowStartnode(param.value) - //} - /* - const sub = responseJson.find(data => data.id === param.value) - setSubworkflow(sub) - } - */ - } - } - } - } - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - - function OuterLink(props) { - if (props.href.includes("http") || props.href.includes("mailto")) { - return {props.children} - } - return {props.children} - } - - function Img(props) { - return {props.alt} - } - - function CodeHandler(props) { - return ( -
    -				
    -					{props.value}
    -				
    -			
    - ) - } - - function Heading(props) { - const element = React.createElement(`h${props.level}`, {style: {marginTop: 40}}, props.children) - return ( - - {props.level !== 1 ? : null} - {element} - - ) - } - - const generateApikey = () => { - fetch(globalUrl+"/api/v1/generateapikey", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for APIKEY gen :O!") - } - - return response.json() - }) - .then((responseJson) => { - setUserSettings(responseJson) - }) - .catch(error => { - console.log(error) - }); - } - - const getSettings = () => { - fetch(globalUrl+"/api/v1/getsettings", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for get settings :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.apikey === undefined || responseJson.apikey.length === 0 || responseJson.apikey === null) { - generateApikey() - } - setUserSettings(responseJson) - }) - .catch(error => { - console.log(error) - }); - } - - const setNewAppAuth = (appAuthData) => { - console.log("DAta: ", appAuthData) - fetch(globalUrl+"/api/v1/apps/authentication", { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(appAuthData), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for setting app auth :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success) { - alert.error("Failed to set app auth: "+responseJson.reason) - } else { - getAppAuthentication(true, false) - setAuthenticationModalOpen(false) - - // Needs a refresh with the new authentication.. - //alert.success("Successfully saved new app auth") - } - }) - .catch(error => { - alert.error(error.toString()) - }) - } - - const getWorkflowExecution = (id, execution_id) => { - fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.length > 0) { - // FIXME: Sort this by time - - // - means it's opposite - const newkeys = sortByKey(responseJson, "-started_at") - setWorkflowExecutions(newkeys) - //console.log("NEWKEYS: ", newkeys) - //setWorkflowExecutions(responseJson) - - const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search - var tmpView = new URLSearchParams(cursearch).get("execution_id") - if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) { - tmpView = execution_id - } - - if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { - const execution = responseJson.find(data => data.execution_id === tmpView) - if (execution !== null && execution !== undefined) { - setExecutionData(execution) - setExecutionModalView(1) - start() - - setExecutionRequest({ - "execution_id": execution.execution_id, - "authorization": execution.authorization, - }) - - const newitem = removeParam("execution_id", cursearch) - props.history.push(curpath+newitem) - } - } - } - - //alert.info("Loaded executions") - //setWorkflowExecutions(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const debugView = workflowExecutions.length > 0 ? - -
    - {workflowExecutions.slice(0,15).map((data, index) => { - return ( -
    - {new Date(data.started_at*1000).toISOString()} - , {data.status} - {data.result.length > 0 ? ", "+data.result : ", "} - {data.execution_argument.length > 0 ? ", "+data.execution_argument : ", "} - -
    - ) - return - })} -
    -
    - : null - - const fetchUpdates = () => { - fetch(globalUrl+"/api/v1/streams/results", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(executionRequest), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for stream results :O!") - stop() - } - - return response.json() - }) - .then((responseJson) => { - handleUpdateResults(responseJson, executionRequest) - }) - .catch(error => { - console.log("Error: ", error) - //alert.error(error.toString()) - stop() - }) - } - - const abortExecution = () => { - setExecutionRunning(false) - - //alert.info("Aborting execution") - fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/executions/"+executionRequest.execution_id+"/abort", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for ABORT EXECUTION :O!") - } else { - //alert.success("Execution aborted") - } - - return response.json() - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - // Controls the colors and direction of execution results. - // Style is in defaultCytoscapeStyle.js - const handleUpdateResults = (responseJson, executionRequest) => { - //console.log(responseJson) - // Loop nodes and find results - // Update on every interval? idk - - if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) { - // FIXME: If another is selected, don't edit.. - // Doesn't work because this is some async garbage - if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { - - if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || executionData.results.length !== responseJson.results.length) { - //console.log("Updated state with this data:") - //console.log(responseJson) - //console.log(executionData) - setExecutionData(responseJson) - } else { - console.log("NOT updating state.") - } - } - } else { - //console.log("JSON is same") - } - - //console.log("PRE LOOPING RESULTS: !", responseJson.execution_id, executionRequest.execution_id) - - if (responseJson.execution_id !== executionRequest.execution_id) { - cy.elements().removeClass('success-highlight failure-highlight executing-highlight') - return - } - - //console.log("LOOPING RESULTS!") - - if (responseJson.results !== null && responseJson.results.length > 0) { - for (var key in responseJson.results) { - var item = responseJson.results[key] - var currentnode = cy.getElementById(item.action.id) - if (currentnode.length === 0) { - continue - } - - currentnode = currentnode[0] - const outgoingEdges = currentnode.outgoers('edge') - const incomingEdges = currentnode.incomers('edge') - - //currentnode.removeClass('success-highlight failure-highlight executing-highlight') - switch (item.status) { - case "EXECUTING": - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('success-highlight') - currentnode.removeClass('failure-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.removeClass('awaiting-data-highlight') - incomingEdges.addClass('success-highlight') - currentnode.addClass('executing-highlight') - break - case "SKIPPED": - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('success-highlight') - currentnode.removeClass('failure-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.removeClass('awaiting-data-highlight') - currentnode.removeClass('executing-highlight') - currentnode.addClass('skipped-highlight') - break - case "WAITING": - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('success-highlight') - currentnode.removeClass('failure-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.removeClass('awaiting-data-highlight') - currentnode.addClass('executing-highlight') - - if (!visited.includes(item.action.label)) { - if (executionRunning) { - //alert.show("WAITING FOR "+item.action.label+" with result "+item.result) - visited.push(item.action.label) - setVisited(visited) - } - } - - // FIXME - add outgoing nodes to executing - //const outgoingNodes = outgoingEdges.find().data().target - if (outgoingEdges.length > 0) { - outgoingEdges.addClass('success-highlight') - } - break - case "SUCCESS": - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('executing-highlight') - currentnode.removeClass('failure-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.removeClass('awaiting-data-highlight') - currentnode.addClass('success-highlight') - incomingEdges.addClass('success-highlight') - outgoingEdges.addClass('success-highlight') - - if (visited !== undefined && visited !== null && !visited.includes(item.action.label)) { - if (executionRunning) { - //alert.show("Success in node "+item.action.label) - //+" with result "+item.result) - visited.push(item.action.label) - setVisited(visited) - } - } - - // FIXME - add outgoing nodes to executing - //const outgoingNodes = outgoingEdges.find().data().target - if (outgoingEdges.length > 0) { - for (var i = 0; i < outgoingEdges.length; i++) { - const edge = outgoingEdges[i] - const targetnode = cy.getElementById(edge.data().target) - if (targetnode !== undefined && !targetnode.classes().includes("success-highlight") && !targetnode.classes().includes("failure-highlight")) { - targetnode.removeClass('not-executing-highlight') - targetnode.removeClass('success-highlight') - targetnode.removeClass('shuffle-hover-highlight') - targetnode.removeClass('failure-highlight') - targetnode.removeClass('awaiting-data-highlight') - targetnode.addClass('executing-highlight') - } - } - - // const outgoingEdges = currentnode.outgoers('edge') - } - break - case "FAILURE": - //When status comes as failure, allow user to start workflow execution - if (executionRunning) { - setExecutionRunning(false) - } - - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('executing-highlight') - currentnode.removeClass('success-highlight') - currentnode.removeClass('awaiting-data-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.addClass('failure-highlight') - - if (!visited.includes(item.action.label)) { - if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { - alert.error("Error for "+item.action.label+" with result "+item.result) - } - visited.push(item.action.label) - setVisited(visited) - } - break - case "AWAITING_DATA": - currentnode.removeClass('not-executing-highlight') - currentnode.removeClass('executing-highlight') - currentnode.removeClass('success-highlight') - currentnode.removeClass('failure-highlight') - currentnode.removeClass('shuffle-hover-highlight') - currentnode.addClass('awaiting-data-highlight') - break - default: - console.log("DEFAULT?") - break - } - } - } - - if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status == "WAITING") { - stop() - - if (executionRunning) { - setExecutionRunning(false) - } - - var curelements = cy.elements() - for (var i = 0; i < curelements.length; i++) { - if (curelements[i].classes().includes("executing-highlight")) { - curelements[i].removeClass("executing-highlight") - curelements[i].addClass("failure-highlight") - } else { - //curelements[i].removeClass('not-executing-highlight') - //curelements[i].removeClass('executing-highlight') - //curelements[i].removeClass('success-highlight') - //curelements[i].removeClass('awaiting-data-highlight') - //curelements[i].removeClass('failure-highlight') - } - } - - getWorkflowExecution(props.match.params.key, "") - } else if (responseJson.status === "FINISHED") { - //console.log("STOPPING BECAUSE ITS OVAH!") - setExecutionRunning(false) - stop() - getWorkflowExecution(props.match.params.key, "") - setUpdate(Math.random()) - } else { - //console.log("Nothing to update") - } - } - - const saveWorkflow = (curworkflow, executionArgument, startNode) => { - 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` - return - } - - setSavingState(2) - - // This might not be the right course of action, but seems logical, as items could be running already - // Makes it possible to update with a version in current render - stop() - var useworkflow = workflow - if (curworkflow !== undefined) { - useworkflow = curworkflow - } else { - //alert.info("Saving workflow") - } - - var cyelements = cy.elements() - var newActions = [] - var newTriggers = [] - var newBranches = [] - var newVBranches = [] - for (var key in cyelements) { - if (cyelements[key].data === undefined) { + for (var key in jsonInput) { + if (!jsonInput.hasOwnProperty(key)) { continue } - var type = cyelements[key].data()["type"] - if (type === undefined) { - if (cyelements[key].data().source === undefined || - cyelements[key].data().target === undefined) { - continue - } - - var parsedElement = { - id: cyelements[key].data().id, - source_id: cyelements[key].data().source, - destination_id: cyelements[key].data().target, - conditions: cyelements[key].data().conditions, - decorator: cyelements[key].data().decorator, - } - - if (parsedElement.decorator) { - newVBranches.push(parsedElement) - } else { - newBranches.push(parsedElement) + const value = jsonInput[key]; + // Check if array + if (Array.isArray(value)) { + for (var i = 0; i < value.length; i++) { + jsonInput[key][i] = SetJsonDotnotation(jsonInput[key][i], inputKey+"."+key+".#"); } + } else if (typeof value === "object") { + jsonInput[key] = SetJsonDotnotation(jsonInput[key], inputKey+"."+key); } else { - if (type === "ACTION") { - const cyelement = cyelements[key].data() - const elementid = cyelement.id === undefined || cyelement.id === null ? cyelement["_id"] : cyelement.id - - var curworkflowAction = useworkflow.actions.find(a => a !== undefined && (a["id"] === elementid || a["_id"] === elementid)) - if (curworkflowAction === undefined) { - curworkflowAction = cyelements[key].data() - } - - curworkflowAction.position = cyelements[key].position() - - // workaround to fix some edgecases - if (curworkflowAction.parameters === "" || curworkflowAction.parameters === null) { - curworkflowAction.parameters = [] - } - - if (curworkflowAction.example === undefined || curworkflowAction.example === "" || curworkflowAction.example === null) { - if (cyelements[key].data().example !== undefined) { - curworkflowAction.example = cyelements[key].data().example - } - } - - // Override just in this place - curworkflowAction.errors = [] - curworkflowAction.isValid = true - - // Cleans up OpenAPI items - var newparams = [] - for (var key in curworkflowAction.parameters) { - const thisitem = curworkflowAction.parameters[key] - if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { - continue - } - - newparams.push(thisitem) - } - - curworkflowAction.parameters = newparams - newActions.push(curworkflowAction) - } else if (type === "TRIGGER") { - //console.log("TRIGGER") - var curworkflowTrigger = useworkflow.triggers.find(a => a.id === cyelements[key].data()["id"]) - if (curworkflowTrigger === undefined) { - curworkflowTrigger = cyelements[key].data() - } - - curworkflowTrigger.position = cyelements[key].position() - //console.log(curworkflowTrigger) - - newTriggers.push(curworkflowTrigger) - } + jsonInput[key] = inputKey+"."+key } } + } else { + //jsonInput = inputKey - useworkflow.actions = newActions - useworkflow.triggers = newTriggers - useworkflow.branches = newBranches - useworkflow.visual_branches = newVBranches - - // Errors are backend defined - useworkflow.errors = [] - useworkflow.previously_saved = true - - setLastSaved(true) - fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(useworkflow), - credentials: "include", - }) - .then((response) => { - setSavingState(0) - if (response.status !== 200) { - console.log("Status not 200 for setting workflows :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (executionArgument !== undefined && startNode !== undefined) { - console.log("Running execution AFTER saving") - executeWorkflow(executionArgument, startNode, true) - return - } - - if (!responseJson.success) { - console.log(responseJson) - alert.error("Failed to save: "+responseJson.reason) - } else { - if (responseJson.new_id !== undefined && responseJson.new_id !== null) { - window.location.pathname = "/workflows/"+responseJson.new_id - } - - success = true - if (responseJson.errors !== undefined) { - //console.log(responseJson) - workflow.errors = responseJson.errors - if (responseJson.errors.length === 0) { - workflow.isValid = true - workflow.is_valid = true - - //console.log("ELEMENTS: ", cy.elements()) - //const setupGraph = () => { - const cyelements = cy.elements() - for (var i = 0; i < cyelements.length; i++) { - cyelements[i].removeStyle() - cyelements[i].data().is_valid = true - cyelements[i].data().errors = [] - } - - for (var key in workflow.actions) { - workflow.actions[key].is_valid = true - workflow.actions[key].errors = [] - } - } - - for (var key in workflow.errors) { - //console.log("Error: ", workflow.errors[key]) - alert.info(workflow.errors[key]) - } - - setWorkflow(workflow) - } - - //alert.success("Successfully saved workflow") - setSavingState(1) - setTimeout(() => { - setSavingState(0) - }, 1500); - } - }) - .catch(error => { - setSavingState(0) - alert.error(error.toString()) - }); - - return success + console.log("SetJsonDotnotation: jsonInput is not an object or array, but key ", jsonInput, typeof jsonInput); } - const monitorUpdates = () => { - var firstnode = cy.getElementById(workflow.start) - if (firstnode.length === 0) { - var found = false - for (var key in workflow.actions) { - if (workflow.actions[key].isStartNode) { - console.log("Updating startnode") - workflow.start = workflow.actions[key].id - firstnode = cy.getElementById(workflow.actions[key].id) - found = true - break - } - } + return jsonInput; +} - if (!found) { - return false - } - } +export const green = "#86c142"; +export const yellow = "#FECC00"; - cy.elements().removeClass('success-highlight failure-highlight executing-highlight') - firstnode[0].addClass('executing-highlight') +export function removeParam(key, sourceURL) { + if (sourceURL === undefined) { + return; + } - return true - } + var rtn = sourceURL.split("?")[0], + param, + params_arr = [], + queryString = sourceURL.indexOf("?") !== -1 ? sourceURL.split("?")[1] : ""; - const executeWorkflow = (executionArgument, startNode, hasSaved) => { - if (hasSaved === false) { - //alert.error("You might have forgotten to save before executing.") - //const saveWorkflow = (curworkflow) => { - //setExecutionRunning(true) - setExecutionRequestStarted(true) - saveWorkflow(workflow, executionArgument, startNode) - console.log("FIXME: Might have forgotten to save before executing.") - return - } + if (queryString !== "") { + params_arr = queryString.split("&"); + for (let i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split("=")[0]; + if (param === key) { + params_arr.splice(i, 1); + } + } + rtn = rtn + "?" + params_arr.join("&"); + } - if (workflow.public) { - alert.info("Save it to get a new version") - } + if (rtn === "?") { + return ""; + } - var returncheck = monitorUpdates() - if (!returncheck) { - alert.error("No startnode set.") - return - } + return rtn; +} - setVisited([]) - setExecutionRequest({}) - stop() +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid #f85a3e", + color: "white", + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: "white", + // This matches the specificity of the default styles at https://github.com/mui-org/material-ui/blob/v4.11.3/packages/material-ui-lab/src/Autocomplete/Autocomplete.js#L90 + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, +}); - var curelements = cy.elements() - for (var i = 0; i < curelements.length; i++) { - curelements[i].addClass("not-executing-highlight") - } +const splitter = "|~|"; +const svgSize = 24; +//const referenceUrl = "https://shuffler.io/functions/webhooks/" +//const referenceUrl = window.location.origin+"/api/v1/hooks/" - if (executionArgument !== undefined && executionArgument !== null && executionArgument.length > 0) { - //alert.success("Starting execution WITH an execution argument") - } else { - //alert.success("Starting execution") - } +const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const AngularWorkflow = (defaultprops) => { + const { globalUrl, isLoggedIn, isLoaded, userdata, data_id } = defaultprops; + const referenceUrl = globalUrl + "/api/v1/hooks/"; + const alert = useAlert() + let navigate = useNavigate(); + const params = useParams(); + var props = JSON.parse(JSON.stringify(defaultprops)) + props.match = {} + props.match.params = params - 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', - }, - credentials: "include", - body: JSON.stringify(data), - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } + //const theme = useTheme(); - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success) { - alert.error("Failed to start: "+responseJson.reason) - setExecutionRunning(false) - setExecutionRequestStarted(false) - stop() - for (var i = 0; i < curelements.length; i++) { - curelements[i].removeClass("not-executing-highlight") - } - return - } else { - setExecutionRunning(true) - setExecutionRequestStarted(false) - } + var to_be_copied = ""; + const [firstrequest, setFirstrequest] = React.useState(true); + const [cystyle] = useState(cytoscapestyle); + const [cy, setCy] = React.useState(); - if (responseJson.execution_id === "" || responseJson.execution_id === undefined || responseJson.authorization === "" || responseJson.authorization === undefined ) { - alert.error("Something went wrong during execution startup") - console.log("BAD RESPONSE FOR EXECUTION: ", responseJson) - setExecutionRunning(false) - setExecutionRequestStarted(false) - stop() + const [toolsApp, setToolsApp] = React.useState({}); + const [currentView, setCurrentView] = React.useState(0); + const [triggerAuthentication, setTriggerAuthentication] = React.useState({}); + const [triggerFolders, setTriggerFolders] = React.useState([]); + const [workflows, setWorkflows] = React.useState([]); + const [parentWorkflows, setParentWorkflows] = React.useState([]); + const [showEnvironment, setShowEnvironment] = React.useState(false); + const [editWorkflowDetails, setEditWorkflowDetails] = React.useState(false); - for (var i = 0; i < curelements.length; i++) { - curelements[i].removeClass("not-executing-highlight") - } - return - } + const [workflow, setWorkflow] = React.useState({}); + const [userSettings, setUserSettings] = React.useState({}); + const [subworkflow, setSubworkflow] = React.useState({}); + const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); + const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); + const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 350); + const [creatorProfile, setCreatorProfile] = React.useState({}); + const [usecases, setUsecases] = React.useState([]); + const [files, setFiles] = React.useState({ + "namespaces": [ + "default", + ] + }); + const [appGroup, setAppGroup] = React.useState([]); + const [triggerGroup, setTriggerGroup] = React.useState([]); + const [executionText, setExecutionText] = React.useState(""); + const [executionRequestStarted, setExecutionRequestStarted] =React.useState(false); + + const [scrollConfig, setScrollConfig] = React.useState({ + top: 0, + left: 0, + selected: "", + }); - setExecutionRequest({ - "execution_id": responseJson.execution_id, - "authorization": responseJson.authorization, - }) - setExecutingNodes([workflow.start]) - setExecutionData({}) - setExecutionModalOpen(true) - setExecutionModalView(1) - start() - }) - .catch(error => { - alert.error(error.toString()) - }); - } + const [history, setHistory] = React.useState([]); + const [historyIndex, setHistoryIndex] = React.useState(history.length); + const [variableInfo, setVariableInfo] = React.useState({}) - //const handleAppVersioning = (apps) => { - // var newapps = [] - // for (var key in apps) { - // var item = apps[key] - // const previtem = newapps.findIndex(data => data.name === item.name) - // if (previtem === -1) { - // item["versions"] = [item.app_version] - // newapps.push(item) - // continue - // } + const [appAuthentication, setAppAuthentication] = React.useState(undefined); + const [variablesModalOpen, setVariablesModalOpen] = React.useState(false); + const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = + React.useState(false); + const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); + + const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false); + const [authenticationType, setAuthenticationType] = React.useState(""); + + + const [workflowDone, setWorkflowDone] = React.useState(false); + const [localFirstrequest, setLocalFirstrequest] = React.useState(true); + const [requiresAuthentication, setRequiresAuthentication] = + React.useState(false); + const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false); + const [showSkippedActions, setShowSkippedActions] = React.useState(false); + const [lastExecution, setLastExecution] = React.useState(""); + const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false); + + const curpath = + typeof window === "undefined" || window.location === undefined + ? "" + : window.location.pathname; + + // 0 = normal, 1 = just done, 2 = normal + const [savingState, setSavingState] = React.useState(0); + + const [selectedResult, setSelectedResult] = React.useState({}); + const [codeModalOpen, setCodeModalOpen] = React.useState(false); + + const [variableAnchorEl, setVariableAnchorEl] = React.useState(null); + + const [sourceValue, setSourceValue] = React.useState({}); + const [destinationValue, setDestinationValue] = React.useState({}); + const [conditionValue, setConditionValue] = React.useState({}); + const [dragging, setDragging] = React.useState(false); + const [dragPosition, setDragPosition] = React.useState({ + x: 0, + y: 0, + }); + + // Trigger stuff + const [selectedComment, setSelectedComment] = React.useState({}); + const [selectedTrigger, setSelectedTrigger] = React.useState({}); + const [selectedTriggerIndex, setSelectedTriggerIndex] = React.useState({}); + const [selectedEdge, setSelectedEdge] = React.useState({}); + const [selectedEdgeIndex, setSelectedEdgeIndex] = React.useState({}); + + const [visited, setVisited] = React.useState([]); + + const [apps, setApps] = React.useState([]); + const [filteredApps, setFilteredApps] = React.useState([]); + const [prioritizedApps, setPrioritizedApps] = React.useState([]); + + const [environments, setEnvironments] = React.useState([]); + const [established, setEstablished] = React.useState(false); + const [setupSent, setSetupSent] = React.useState(false); + + const [graphSetup, setGraphSetup] = React.useState(false); + + const [selectedApp, setSelectedApp] = React.useState({}); + const [selectedAction, setSelectedAction] = React.useState({}); + const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); + + const [executionRequest, setExecutionRequest] = React.useState({}); + + const [executionRunning, setExecutionRunning] = React.useState(false); + const [executionModalOpen, setExecutionModalOpen] = React.useState(false); + const [executionModalView, setExecutionModalView] = React.useState(0); + const [executionData, setExecutionData] = React.useState({}); + const [appsLoaded, setAppsLoaded] = React.useState(false); + const [showVideo, setShowVideo] = React.useState(""); + const [editWorkflowModalOpen, setEditWorkflowModalOpen] = React.useState(false); + const [userediting, setUserediting] = React.useState(false) + + const [lastSaved, setLastSaved] = React.useState(true); + const [selectionOpen, setSelectionOpen] = React.useState(false); + + // eslint-disable-next-line no-unused-vars + const [_, setUpdate] = useState(""); // Used for rendring, don't remove + + const [workflowExecutions, setWorkflowExecutions] = React.useState([]); + const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0); + const [workflowRecommendations, setWorkflowRecommendations] = React.useState([]); + + const [listCache, setListCache] = React.useState([]); + const [suggestionBox, setSuggestionBox] = React.useState({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", + }); - // // THere might be duplicates for some reason.. - // if (!newapps[previtem]["versions"].includes(item.app_version)) { - // newapps[previtem]["versions"].push(item.app_version) - // } - // } - // - // // FIXME - handle this, as we can't have more than one of each :) - // //setVersionedApps(newapps) - //} - // Builtin actions that should ran in Worker and not apps - const getExtraApps = () => { - const data = [{ - name: "Filter", - is_valid: true, - id: "0ca8887e-b4af-4e3e-887c-87e9d3bc3d3e", - link: "https://shuffler.io", - app_version: "1.0.0", - generated: true, - downloaded: false, - sharing: false, - verified: false, - tested: false, - owner: "", - private_id: "", - description: "Filter", - environment: "Shuffle", - small_image: "", - large_image: "", - contact_info: {name: "", url: ""}, - authentication: {required: false, parameters: [],}, - actions: [{ - description: "Filter cases", - id: "", - name: "filter_cases", - node_type: "action", - environment: "Shuffle", - sharing: false, - private_id: "", - app_id: "", - authentication: null, - tested: true, - parameters: [{ - description: "", - id: "", - name: "Field to look for", - example: "$testing_1.#.id", - value: "", - multiline: true, - action_field: "", - variant: "", - required: true, - schema: {type: "string"}, - }] - }], - }] - return data - } + // This should all be set once, not on every iteration + // Use states and don't update lol + const cloudSyncEnabled = + props.userdata !== undefined && + props.userdata.active_org !== null && + props.userdata.active_org !== undefined + ? props.userdata.active_org.cloud_sync === true + : false; + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; - // This can be used to only show prioritzed ones later - // Right now, it can prioritize authenticated ones - //"Testing", - const internalIds = [ - "Shuffle Tools", - "http", - ] + const appBarSize = isCloud ? 75 : 72; + const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"]; + const unloadText = "Are you sure you want to leave without saving (CTRL+S)?"; + const classes = useStyles(); - const getAppAuthentication = (reset, updateAction) => { - fetch(globalUrl+"/api/v1/apps/authentication", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", + const [bodyWidth, bodyHeight] = useWindowSize() + //console.log("Mobile: ", isMobile, bodyWidth, bodyHeight) + const cytoscapeWidth = isMobile ? bodyWidth - leftBarSize : bodyWidth - leftBarSize - 25 + + const [elements, setElements] = useState([]); + // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. + // Set it from 2500 to 6000 to reduce overall load + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + fetchUpdates(); + }, + }); + + const getAppDocs = (appname, location, version) => { + fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, { + headers: { + Accept: "application/json", + }, + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - } + .then((response) => { + if (response.status === 200) { + //alert.success("Successfully GOT app "+appId) + } else { + //alert.error("Failed getting app"); + } - return response.json() + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + selectedApp.documentation = responseJson.reason + setSelectedApp(selectedApp) + setUpdate(Math.random()) + } + } + } + + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + useEffect(() => { + if (authenticationModalOpen === true && selectedAction.app_name !== undefined) { + console.log(`Should get app docs for: ${selectedAction.app_name}`) + //console.log(selectedAction) + //console.log("APP: ", selectedApp) + + if (selectedAction.documentation === undefined || selectedAction.documentation === null || selectedAction.documentation.length === 0) { + // SelectedApp.documentation = Markdown? If so, it works + // + const apptype = selectedApp.generated === false ? "python" : "openapi" + getAppDocs(selectedAction.app_name, apptype, selectedAction.app_version) + } + } + }, [authenticationModalOpen]) + + const listOrgCache = (orgId) => { + fetch(`${globalUrl}/api/v1/orgs/${orgId}/list_cache`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .then((responseJson) => { - if (responseJson.success) { - var newauth = [] - for (var key in responseJson.data) { - if (responseJson.data[key].defined === false) { - continue - } - - newauth.push(responseJson.data[key]) - } - - //{selectedAction.authentication.map(data => { - - if (cy !== undefined) { - console.log("NEW AUTH = reset cy's onnodeselect") - - // 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)) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; } - setAppAuthentication(newauth) - setAuthLoaded(true) + return response.json(); + }) + .then((responseJson) => { + setListCache(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; - if (updateAction === true) { - //console.log("Should update authentication for selectedAction!!") - //console.log(responseJson) - - if (selectedApp.authentication.required) { - //console.log("App requires auth!!") - // Setup auth here :) - var appUpdates = false - const authenticationOptions = [] - var findAuthId = "" - if (selectedAction.authentication_id !== null && selectedAction.authentication_id !== undefined && selectedAction.authentication_id.length > 0) { - findAuthId = selectedAction.authentication_id - } + 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) { - var tmpAuth = JSON.parse(JSON.stringify(responseJson.data)) - //console.log("FOUND AUTH: ", tmpAuth) + // Sets up subflow trigger with the right info + if (trigger_index > -1) { - //console.log("Checking authentication: ", tmpAuth) - var latest = 0 - for (var key in tmpAuth) { - var item = tmpAuth[key] + var baseSubflow = {} + const trigger = workflow.triggers[trigger_index]; + if (trigger.parameters.length >= 3) { + for (let paramkey in trigger.parameters) { + const param = trigger.parameters[paramkey]; - const newfields = {} - for (var filterkey in item.fields) { - newfields[item.fields[filterkey].key] = item.fields[filterkey].value - } + // 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) - item.fields = newfields - if (item.app.name === selectedApp.name) { - authenticationOptions.push(item) + 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); + } + } + } + } - // Always becoming the last one - if (item.edited > latest) { - latest = item.edited - selectedAction.selectedAuthentication = item + if (param.name === "startnode" && param.value !== undefined && param.value !== null) { + + if (Object.getOwnPropertyNames(baseSubflow).length > 0) { + const foundAction = baseSubflow.actions.find(action => action.id === param.value) + if (foundAction !== null && foundAction !== undefined) { + setSubworkflowStartnode(foundAction); + } + } else { + setSubworkflowStartnode(param.value); + } + } + } + } + } - for (var key in workflow.actions) { - console.log(workflow.actions[key].app_name) - if (workflow.actions[key].app_name == selectedApp.name) { - console.log("Setting auth at: ", workflow.actions[key], item.id) - workflow.actions[key].selectedAuthentication = item - workflow.actions[key].authentication_id = item.id - appUpdates = true - //if (workflow.actions[key].selectedAuthentication === undefined || workflow.actions[key].selectedAuthentication === null || workflow.actions[key].selectedAuthentication.length === 0) { - // console.log("Setting inner auth: ", workflow.actions[key]) - //} + if (workflows.length === 0) { + //console.log("First request. Checking for parent trigger (if this is subflow") + var parentworkflows = [] + var parent_ids = [] + for (let workflowkey in responseJson) { + const innerworkflow = responseJson[workflowkey] + + for (let triggerkey in innerworkflow.triggers) { + const trigger = innerworkflow.triggers[triggerkey] + if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") { + + for (let paramkey in trigger.parameters) { + const param = trigger.parameters[paramkey] + if ((param.name === "workflow" || param.name === "subflow") && param.value === props.match.params.key && !parent_ids.includes(innerworkflow.id)) { + + parent_ids.push(innerworkflow.id) + parentworkflows.push({ + id: innerworkflow.id, + name: innerworkflow.name, + image: innerworkflow.image, + }) } } } } } - //if (item.id === findAuthId) { - // selectedAction.selectedAuthentication = item - //} - //console.log("ACTION: ", selectedAction) - - //console.log("OPTIONS: ", authenticationOptions) - selectedAction.authentication = authenticationOptions - //console.log("Authentication: ", authenticationOptions) - if (selectedAction.selectedAuthentication === null || selectedAction.selectedAuthentication === undefined || selectedAction.selectedAuthentication.length === "") { - selectedAction.selectedAuthentication = {} + if (parentworkflows.length > 0) { + setParentWorkflows(parentworkflows.filter(wf => wf.id !== props.match.params.key)) } - - - if (appUpdates === true) { - setAuthenticationModalOpen(false) - setSelectedAction(selectedAction) - setWorkflow(workflow) - saveWorkflow(workflow) - alert.info("Added and updated authentication!") - } else { - alert.error("Failed to find new authentication - did it work?") - } - } else { - alert.info("No authentication to update") } - } - } else { - setAuthLoaded(true) - //alert.error("Failed getting authentications") - } - }) - .catch(error => { - setAuthLoaded(true) - alert.error("Auth loading error: "+error.toString()) - }) - } + setWorkflows(responseJson); + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Workflow error: ", error.toString()) + }); + }; - const getApps = () => { - fetch(globalUrl+"/api/v1/apps", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", + function OuterLink(props) { + if (props.href.includes("http") || props.href.includes("mailto")) { + return ( + + {props.children} + + ); + } + return ( + + {props.children} + + ); + } + + function Img(props) { + return {props.alt}; + } + + function CodeHandler(props) { + return ( +
    +        {props.value}
    +      
    + ); + } + + function Heading(props) { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: 40 } }, + props.children + ); + return ( + + {props.level !== 1 ? ( + + ) : null} + {element} + + ); + } + + const generateApikey = () => { + fetch(globalUrl + "/api/v1/generateapikey", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for APIKEY gen :O!"); + } - return response.json() - }) - .then((responseJson) => { - // FIXME - handle versions on left bar - //handleAppVersioning(responseJson) - //var tmpapps = [] - //tmpapps = tmpapps.concat(getExtraApps()) - //tmpapps = tmpapps.concat(responseJson) - console.log("APPS: ", responseJson) - setApps(responseJson) + return response.json(); + }) + .then((responseJson) => { + setUserSettings(responseJson); + }) + .catch((error) => { + console.log("Apikey error: ", error); + }); + }; - if (isCloud) { - setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name))) - setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name))) - } else { - setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated))) - setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name))) - } + const getSettings = () => { + fetch(globalUrl + "/api/v1/getsettings", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const getWorkflow = (workflow_id, sourcenode) => { - console.log(`Getting workflow ${workflow_id} with append value ${sourcenode}`) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for get settings :O!"); + } - fetch(globalUrl+"/api/v1/workflows/"+workflow_id, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", + return response.json(); + }) + .then((responseJson) => { + if ( + responseJson.success === true && + (responseJson.apikey === undefined || + responseJson.apikey.length === 0 || + responseJson.apikey === null) + ) { + generateApikey(); + } + + if (responseJson.success === true) { + setUserSettings(responseJson) + } + }) + .catch((error) => { + console.log("Settings error: ", error); + }); + }; + + const setNewAppAuth = (appAuthData) => { + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(appAuthData), + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflows :O!") - window.location.pathname = "/workflows" - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + } - return response.json() - }) - .then((responseJson) => { - // Not sure why this is necessary. - if (responseJson.isValid === undefined) { - responseJson.isValid = true - } + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + alert.error("Failed to set app auth: " + responseJson.reason); + } else { + getAppAuthentication(true, false); + setAuthenticationModalOpen(false); - if (responseJson.errors === undefined) { - responseJson.errors = [] - } + // Needs a refresh with the new authentication.. + //alert.success("Successfully saved new app auth") + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("New auth error: ", error.toString()); + }); + }; - if (responseJson.public) { - alert.info("This workflow is public. You will have to save it to make it your own!") - //setLastSaved(false) - } - - // Appends SUBFLOWS. Does NOT run during normal grabbing of workflows. - if (sourcenode.id !== undefined) { - console.log("WORKFLOW: ", responseJson) - - var nodefound = false - const target = sourcenode.parameters.find(item => item.name === "startnode") - console.log(sourcenode.parameters) - console.log(target) - const target_id = target === undefined ? "" : target.value - const actions = responseJson.actions.map(action => { - const node = { - group: "nodes", - } - - // Set it dynamically? - node.position = { - x: sourcenode.position.x+action.position.x, - y: sourcenode.position.y+action.position.y, - } - - node.data = action - - node.data._id = action["id"] - node.data.type = "ACTION" - node.data.source_workflow = responseJson.id - if (action.id === target_id) { - nodefound = true - } - - var example = "" - if (action.example !== undefined && action.example !== null && action.example.length > 0) { - example = action.example - } - - node.data.example = example - return node; - }) - - var edges = responseJson.branches.map((branch, index) => { - const edge = { }; - var conditions = responseJson.branches[index].conditions - if (conditions === undefined || conditions === null) { - conditions = [] - } - - var label = "" - if (conditions.length === 1) { - label = conditions.length+" condition" - } else if (conditions.length > 1) { - label = conditions.length+" conditions" - } - - const sourceFound = actions.findIndex(action => action.data.id === branch.source_id) - if (sourceFound < 0) { - return null - } - - const destinationFound = actions.findIndex(action => action.data.id === branch.destination_id) - if (destinationFound < 0) { - return null - } - - edge.data = { - id: branch.id, - _id: branch.id, - source: branch.source_id, - target: branch.destination_id, - label: label, - conditions: conditions, - hasErrors: branch.has_errors, - decorator: false, - source_workflow: responseJson.id, - } - - return edge; - }) - - - //console.log("Adding node: ", node) - //cy.on('add', 'node', (e) => onNodeAdded(e)) - edges = edges.filter(edge => edge !== null) - cy.removeListener('add') - cy.add(actions) - cy.add(edges) - - if (nodefound === true) { - const newId = uuidv4() - cy.add({ - group: "edges", - data: { - id: newId, - _id: newId, - source: sourcenode.id, - target: target_id, - label: "Subflow", - decorator: true, - source_workflow: responseJson.id, - } - }) - } - - cy.fit(null, 100) - //cy.zoom(2.0) - cy.on('add', 'node', (e) => onNodeAdded(e)) - cy.on('add', 'edge', (e) => onEdgeAdded(e)) - - //for (var key in - } else { - setWorkflow(responseJson) - setWorkflowDone(true) - - //console.log(responseJson) - // Add error checks - console.log("Workflow: ", responseJson) - if (!responseJson.public) { - if ((!responseJson.previously_saved || (!responseJson.is_valid || (responseJson.errors !== undefined || responseJson.errors !== null || responseJson.errors !== responseJson.errors.length > 0)))) { - setConfigureWorkflowModalOpen(true) - } - } - } + const getWorkflowExecution = (id, execution_id) => { + fetch(globalUrl + "/api/v1/workflows/" + id + "/executions", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .catch(error => { - alert.error(error.toString()) - }); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } - const onUnselect = (event) => { - console.time("UNSELECT") + return response.json(); + }) + .then((responseJson) => { + if (responseJson.length > 0) { + // FIXME: Sort this by time - const nodedata = event.target.data() - if (nodedata.app_name === undefined && nodedata.source === undefined) { + // - means it's opposite + const newkeys = sortByKey(responseJson, "-started_at"); + setWorkflowExecutions(newkeys); + + const cursearch = + typeof window === "undefined" || window.location === undefined + ? "" + : window.location.search; + + var tmpView = new URLSearchParams(cursearch).get("execution_id"); + if ( + execution_id !== undefined && execution_id !== null && + execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0) + ) { + tmpView = execution_id; + } + + if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { + const execution = responseJson.find((data) => data.execution_id === tmpView); + + if (execution !== null && execution !== undefined) { + + if (execution.execution_argument.includes("too large")) { + setExecutionData({}); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + setExecutionData(execution); + } + + setExecutionModalView(1); + start(); + + setExecutionRequest({ + execution_id: execution.execution_id, + authorization: execution.authorization, + }); + + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + //props.history.push(curpath + newitem); + } else { + console.log("Couldn't find execution for execution ID. Retrying as user to get ", tmpView) + + //setExecutionRequestStarted(true); + const cur_execution = { + execution_id: tmpView, + //authorization: data.authorization, + } + setExecutionModalView(1); + setExecutionRequest(cur_execution); + start(); + + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + + setTimeout(() => { + stop() + }, 5000); + } + } + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get execution error: ", error.toString()); + }); + }; + + const fetchUpdates = () => { + fetch(globalUrl + "/api/v1/streams/results", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(executionRequest), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + stop(); + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + handleUpdateResults(responseJson, executionRequest); + }) + .catch((error) => { + console.log("Execution result Error: ", error); + stop(); + }); + }; + + const abortExecution = () => { + setExecutionRunning(false); + + fetch(globalUrl +"/api/v1/workflows/" +props.match.params.key +"/executions/" +executionRequest.execution_id +"/abort", + { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for ABORT EXECUTION :O!"); + } + + return response.json(); + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Abort error: ", error.toString()); + }); + }; + + const handleColoring = (actionId, status, label) => { + if (cy === undefined) { return } - event.target.removeClass("selected") + var currentnode = cy.getElementById(actionId); + if (currentnode.length === 0) { + return + //continue; + } - // Attempt at rewrite of name in other actions in following nodes. - // Should probably be done in the onBlur for the textfield instead - /* - if (event.target.data().type === "ACTION") { - const nodeaction = event.target.data() - const curaction = workflow.actions.find(a => a.id === nodeaction.id) - console.log("workflowaction: ", curaction) - console.log("nodeaction: ", nodeaction) - if (nodeaction.label !== curaction.label) { - console.log("BEACH!") + currentnode = currentnode[0]; + const outgoingEdges = currentnode.outgoers("edge"); + const incomingEdges = currentnode.incomers("edge"); - var params = [] - const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_") - for (var actionkey in workflow.actions) { - if (workflow.actions[actionkey].id === curaction.id) { - continue + switch (status) { + case "EXECUTING": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + incomingEdges.addClass("success-highlight"); + currentnode.addClass("executing-highlight"); + break; + case "SKIPPED": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.addClass("skipped-highlight"); + break; + case "WAITING": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("executing-highlight"); + + if (!visited.includes(label)) { + if (executionRunning) { + visited.push(label); + setVisited(visited); } + } - for (var paramkey in workflow.actions[actionkey].parameters) { - const param = workflow.actions[actionkey].parameters[paramkey] - if (param.value === null || param.value === undefined || !param.value.includes("$")) { + // FIXME - add outgoing nodes to executing + //const outgoingNodes = outgoingEdges.find().data().target + if (outgoingEdges.length > 0) { + outgoingEdges.addClass("success-highlight"); + } + break; + case "SUCCESS": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("success-highlight"); + incomingEdges.addClass("success-highlight"); + outgoingEdges.addClass("success-highlight"); + + if (visited !== undefined && visited !== null && !visited.includes(label)) { + if (executionRunning) { + visited.push(label); + setVisited(visited); + } + } + + // FIXME - add outgoing nodes to executing + //const outgoingNodes = outgoingEdges.find().data().target + if (outgoingEdges.length > 0) { + for (let i = 0; i < outgoingEdges.length; i++) { + const edge = outgoingEdges[i]; + const targetnode = cy.getElementById(edge.data().target); + if ( + targetnode !== undefined && + !targetnode.classes().includes("success-highlight") && + !targetnode.classes().includes("failure-highlight") + ) { + targetnode.removeClass("not-executing-highlight"); + targetnode.removeClass("success-highlight"); + targetnode.removeClass("shuffle-hover-highlight"); + targetnode.removeClass("failure-highlight"); + targetnode.removeClass("awaiting-data-highlight"); + targetnode.addClass("executing-highlight"); + } + } + } + break; + case "FAILURE": + //When status comes as failure, allow user to start workflow execution + if (executionRunning) { + setExecutionRunning(false); + } + + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.addClass("failure-highlight"); + + if (!visited.includes(label)) { + //if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { + // alert.error("Error for " + item.action.label + " with result " + item.result); + //} + visited.push(label); + setVisited(visited); + } + break; + case "AWAITING_DATA": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.addClass("awaiting-data-highlight"); + break; + default: + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("not-executing-highlight"); + //console.log("DEFAULT -> Clearing!"); + break; + } + } + + // Controls the colors and direction of execution results. + // Style is in defaultCytoscapeStyle.js + const handleUpdateResults = (responseJson, executionRequest) => { + if (responseJson === undefined || responseJson === null || responseJson.success === false) { + return + } + //console.log(responseJson) + // Loop nodes and find results + // Update on every interval? idk + + ReactDOM.unstable_batchedUpdates(() => { + if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) { + // FIXME: If another is selected, don't edit.. + // Doesn't work because this is some async garbage + if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { + if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) { + console.log("Updating data!") + setExecutionData(responseJson) + } else { + console.log("NOT updating executiondata state."); + } + } + } + + if (responseJson.execution_id !== executionRequest.execution_id) { + cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); + return; + } + + if (responseJson.results !== null && responseJson.results.length > 0) { + // First clear current nodes + if (responseJson.workflow.actions !== undefined && responseJson.workflow.actions !== null) { + // In clearing of actions + for (let actionKey in responseJson.workflow.actions) { + var item = responseJson.workflow.actions[actionKey]; + + handleColoring(item.id, "", item.label) + } + } + + for (let resultKey in responseJson.results) { + var item = responseJson.results[resultKey]; + + handleColoring(item.action.id, item.status, item.action.label) + } + } + + if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING") { + stop(); + + if (executionRunning) { + setExecutionRunning(false); + } + + var curelements = cy.elements(); + for (let i = 0; i < curelements.length; i++) { + if (curelements[i].classes().includes("executing-highlight")) { + curelements[i].removeClass("executing-highlight"); + curelements[i].addClass("failure-highlight"); + } + } + + getWorkflowExecution(props.match.params.key, ""); + } else if (responseJson.status === "FINISHED") { + setExecutionRunning(false); + stop(); + getWorkflowExecution(props.match.params.key, ""); + setUpdate(Math.random()); + } + }) + }; + + const sendStreamRequest = (body) => { + console.log("Stream not activated yet.") + return + + // Session may be important here huh + body.user_id = userdata.id + + fetch(globalUrl + "/api/v1/workflows/" + props.match.params.key + "/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + credentials: "include", + }) + .then((response) => { + setSavingState(0); + if (response.status !== 200) { + console.log("Status not 200 for setting workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESP: ", responseJson) + }) + .catch((error) => { + console.log("Stream send error: ", error.toString()) + //alert.error(error.toString()); + }) + } + + const saveWorkflow = (curworkflow, executionArgument, startNode) => { + 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`; + return; + } + + setSavingState(2); + + // This might not be the right course of action, but seems logical, as items could be running already + // Makes it possible to update with a version in current render + stop(); + var useworkflow = workflow; + if (curworkflow !== undefined) { + useworkflow = curworkflow; + } + + var cyelements = cy.elements(); + var newActions = []; + var newTriggers = []; + var newBranches = []; + var newVBranches = []; + var newComments = []; + for (let cyelementsKey in cyelements) { + if (cyelements[cyelementsKey].data === undefined) { + continue; + } + + var type = cyelements[cyelementsKey].data()["type"]; + if (type === undefined) { + if ( + cyelements[cyelementsKey].data().source === undefined || + cyelements[cyelementsKey].data().target === undefined + ) { + continue; + } + + var parsedElement = { + id: cyelements[cyelementsKey].data().id, + source_id: cyelements[cyelementsKey].data().source, + destination_id: cyelements[cyelementsKey].data().target, + conditions: cyelements[cyelementsKey].data().conditions, + decorator: cyelements[cyelementsKey].data().decorator, + }; + + if (parsedElement.decorator) { + newVBranches.push(parsedElement); + } else { + newBranches.push(parsedElement); + } + } else { + if (type === "ACTION") { + const cyelement = cyelements[cyelementsKey].data(); + const elementid = + cyelement.id === undefined || cyelement.id === null + ? cyelement["_id"] + : cyelement.id; + + var curworkflowAction = useworkflow.actions.find( + (a) => + a !== undefined && + (a["id"] === elementid || a["_id"] === elementid) + ); + if (curworkflowAction === undefined) { + curworkflowAction = cyelements[cyelementsKey].data(); + } + + curworkflowAction.position = cyelements[cyelementsKey].position(); + + // workaround to fix some edgecases + if ( + curworkflowAction.parameters === "" || + curworkflowAction.parameters === null + ) { + curworkflowAction.parameters = []; + } + + if ( + curworkflowAction.example === undefined || + curworkflowAction.example === "" || + curworkflowAction.example === null + ) { + if (cyelements[cyelementsKey].data().example !== undefined) { + curworkflowAction.example = cyelements[cyelementsKey].data().example; + } + } + + // Override just in this place + curworkflowAction.errors = []; + curworkflowAction.isValid = true; + + // Cleans up OpenAPI items + var newparams = []; + for (let parametersKey in curworkflowAction.parameters) { + const thisitem = curworkflowAction.parameters[parametersKey]; + if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { + continue; + } + + newparams.push(thisitem); + } + + curworkflowAction.parameters = newparams; + newActions.push(curworkflowAction); + } else if (type === "TRIGGER") { + if (useworkflow.triggers === undefined || useworkflow.triggers === null) { + useworkflow.triggers = []; + } + + var curworkflowTrigger = useworkflow.triggers.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ); + if (curworkflowTrigger === undefined) { + curworkflowTrigger = cyelements[cyelementsKey].data(); + } + + curworkflowTrigger.position = cyelements[cyelementsKey].position(); + if (curworkflowTrigger.canConnect === false) { continue } - const innername = param.value.toLowerCase().replace(" ", "_") - if (innername.includes(fixedName)) { - //workflow.actions[actionkey].parameters[paramkey].replace( - //console.log("FOUND!: ", innername) + newTriggers.push(curworkflowTrigger); + } else if (type === "COMMENT") { + if (useworkflow.comments === undefined || useworkflow.comments === null) { + useworkflow.comments = []; + } + + var curworkflowComment = useworkflow.comments.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ) + + if (curworkflowComment === undefined) { + curworkflowComment = cyelements[cyelementsKey].data(); + try { + curworkflowComment.position.x = parseInt(curworkflowComment.position.x) + } catch (e) { + console.log("Failed to parse position Y of comment: ", curworkflowComment.position.x) + } + + try { + curworkflowComment.position.y = parseInt(curworkflowComment.position.y) + } catch (e) { + console.log("Failed to parse position Y of comment: ", curworkflowComment.position.y) + } + } + + const parsedHeight = parseInt(curworkflowComment["height"]) + if (!isNaN(parsedHeight)) { + curworkflowComment.height = parsedHeight + } else { + curworkflowComment.width = 150 + } + + const parsedWidth = parseInt(curworkflowComment["width"]) + if (!isNaN(parsedWidth)) { + curworkflowComment.width = parsedWidth + } else { + curworkflowComment.width = 200 + } + + curworkflowComment.position = cyelements[cyelementsKey].position(); + //console.log(curworkflowComment) + + newComments.push(curworkflowComment); + } else { + alert.info("No handler for type: " + type); + } + } + } + + if (userediting === true) { + useworkflow.user_editing = true + } + + useworkflow.actions = newActions; + useworkflow.triggers = newTriggers; + useworkflow.branches = newBranches; + useworkflow.comments = newComments; + useworkflow.visual_branches = newVBranches; + + // Errors are backend defined + useworkflow.errors = []; + useworkflow.previously_saved = true; + + if (cy !== undefined) { + // scale: 0.3, + // bg: "#27292d", + const cyImageData = cy.png({ + output: "base64uri", + maxWidth: 480, + maxHeight: 270, + }) + + if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { + useworkflow.image = cyImageData + } + } + + setLastSaved(true); + fetch(`${globalUrl}/api/v1/workflows/${useworkflow.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(useworkflow), + credentials: "include", + }) + .then((response) => { + setSavingState(0); + if (response.status !== 200) { + console.log("Status not 200 for setting workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (executionArgument !== undefined && startNode !== undefined) { + //console.log("Running execution AFTER saving"); + executeWorkflow(executionArgument, startNode, true); + return; + } + + if (!responseJson.success) { + console.log(responseJson); + if (responseJson.reason !== undefined && responseJson.reason !== null) { + alert.error("Failed to save: " + responseJson.reason); + } else { + alert.error("Failed to save. Please contact your support@shuffler.io or your local admin if this is unexpected.") + } + } else { + + sendStreamRequest({ + "item": "workflow", + "type": "save", + "id": workflow.id, + }) + + if ( + responseJson.new_id !== undefined && + responseJson.new_id !== null + ) { + window.location.pathname = "/workflows/" + responseJson.new_id; + } + + success = true; + if (responseJson.errors !== undefined) { + workflow.errors = responseJson.errors; + if (responseJson.errors.length === 0) { + workflow.isValid = true; + workflow.is_valid = true; + + const cyelements = cy.elements(); + + for (let i = 0; i < cyelements.length; i++) { + //cyelements[i].removeStyle(); + cyelements[i].data().is_valid = true; + cyelements[i].data().errors = []; + } + + for (let actionkey in workflow.actions) { + workflow.actions[actionkey].is_valid = true; + workflow.actions[actionkey].errors = []; + } + } + + setWorkflow(workflow); + } + + setSavingState(1); + setTimeout(() => { + setSavingState(0); + }, 1500); + } + }) + .catch((error) => { + setSavingState(0); + //alert.error(error.toString()); + console.log("Save workflow error: ", error.toString()); + }); + + return success; + }; + + const monitorUpdates = () => { + var firstnode = cy.getElementById(workflow.start); + if (firstnode.length === 0) { + var found = false; + for (let actionkey in workflow.actions) { + if (workflow.actions[actionkey].isStartNode) { + console.log("Updating startnode"); + workflow.start = workflow.actions[actionkey].id; + firstnode = cy.getElementById(workflow.actions[actionkey].id); + found = true; + break; + } + } + + if (!found) { + return false; + } + } + + cy.elements().removeClass( + "success-highlight failure-highlight executing-highlight" + ); + firstnode[0].addClass("executing-highlight"); + + return true; + }; + + const executeWorkflow = (executionArgument, startNode, hasSaved) => { + 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) { + alert.info("Save it to get a new version"); + } + + var returncheck = monitorUpdates(); + if (!returncheck) { + alert.error("No startnode set."); + return; + } + + setVisited([]) + setExecutionRequest({}) + stop() + + var curelements = cy.elements(); + for (let i = 0; i < curelements.length; i++) { + curelements[i].addClass("not-executing-highlight"); + } + + 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", + }, + credentials: "include", + body: JSON.stringify(data), + } + ) + .then((response) => { + setExecutionRequestStarted(false) + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + alert.error("Failed to start: " + responseJson.reason); + setExecutionRunning(false); + setExecutionRequestStarted(false); + stop(); + + for (let i = 0; i < curelements.length; i++) { + curelements[i].removeClass("not-executing-highlight"); + } + return; + } else { + setExecutionRunning(true); + setExecutionRequestStarted(false); + } + + if ( + responseJson.execution_id === "" || + responseJson.execution_id === undefined || + responseJson.authorization === "" || + responseJson.authorization === undefined + ) { + alert.error("Something went wrong during execution startup"); + console.log("BAD RESPONSE FOR EXECUTION: ", responseJson); + setExecutionRunning(false); + setExecutionRequestStarted(false); + stop(); + + for (let i = 0; i < curelements.length; i++) { + curelements[i].removeClass("not-executing-highlight"); + } + return; + } + + setExecutionRequest({ + execution_id: responseJson.execution_id, + authorization: responseJson.authorization, + }); + setExecutionData({}); + setExecutionModalOpen(true); + setExecutionModalView(1); + start(); + }) + .catch((error) => { + //alert.error(error.toString()); + setExecutionRequestStarted(false) + console.log("Execute workflow err: ", error.toString()); + }); + }) + }; + + // This can be used to only show prioritzed ones later + // Right now, it can prioritize authenticated ones + //"Testing", + const internalIds = ["Shuffle Tools", "http", "email"]; + + const getAppAuthentication = (reset, updateAction, closeMenu) => { + fetch(globalUrl + "/api/v1/apps/authentication", { + 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) => { + var shouldClose = false + if (responseJson.success) { + var newauth = []; + for (let authkey in responseJson.data) { + if (responseJson.data[authkey].defined === false) { + continue; + } + + newauth.push(responseJson.data[authkey]); + } + + setAppAuthentication(newauth); + + if (cy !== undefined) { + // Remove the old listener for select, run with new one + cy.removeListener("select"); + cy.on("select", "node", (e) => onNodeSelect(e, newauth)); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + } + + if (updateAction === true) { + if (selectedApp.authentication.required) { + // Setup auth here :) + var appUpdates = false; + const authenticationOptions = []; + + var tmpAuth = JSON.parse(JSON.stringify(newauth)); + var latest = 0; + for (let authkey in tmpAuth) { + var item = tmpAuth[authkey]; + + //console.log("Got auth: ", item); + + const newfields = {}; + for (let filterkey in item.fields) { + newfields[item.fields[filterkey].key] = item.fields[filterkey].value; + } + + item.fields = newfields; + + const appname = selectedApp.name.toLowerCase().replace(" ", "_", -1) + const itemname = item.app.name.toLowerCase().replace(" ", "_", -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().replace(" ", "_", -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); + alert.info("Added and updated authentication!"); + shouldClose = true + } else { + console.log("Closing auth modal? FAIL") + + alert.error("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); + shouldClose = false + } + } else { + alert.info("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([]); + //alert.error("Auth loading error: " + error.toString()); + console.log("AppAuth error: " + error.toString()); + }); + }; + + const getApps = () => { + fetch(globalUrl + "/api/v1/apps", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps. Setting to pretend apps!"); + + const pretend_apps = [{ + "name": "TBD", + "app_name": "TBD", + "app_version": "TBD", + "description": "TBD", + "version": "TBD", + "large_image": "", + }] + + setAppsLoaded(true) + setApps(pretend_apps) + setFilteredApps(pretend_apps) + setPrioritizedApps(pretend_apps); + + alert.error("Something went wrong while loading apps. Please refresh the window to try again.") + return + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson === null) { + console.log("No response") + const pretend_apps = [{ + "name": "TBD", + "app_name": "TBD", + "app_version": "TBD", + "description": "TBD", + "version": "TBD", + "large_image": "", + }] + + setAppsLoaded(true) + setApps(pretend_apps) + setFilteredApps(pretend_apps) + setPrioritizedApps(pretend_apps); + return + } + + if (responseJson.success === false) { + return + } + + // Used for e.g. Liquid testing + const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") + if (foundTools !== undefined && foundTools !== null) { + setToolsApp(foundTools) + } + + setApps(responseJson); + + if (isCloud) { + setFilteredApps(responseJson.filter((app) => !internalIds.includes(app.name))); + setPrioritizedApps(responseJson.filter((app) => internalIds.includes(app.name))); + + } else { + //setFilteredApps( + // responseJson.filter( + // (app) => + // !internalIds.includes(app.name) && + // !(!app.activated && app.generated) + // ) + //); + + var tmpFiltered = responseJson.filter((app) => !internalIds.includes(app.name)) + //tmpFiltered = sortByKey(tmpFiltered, "activated") + setFilteredApps(tmpFiltered) + + //!(!app.activated && app.generated) + setPrioritizedApps(responseJson.filter((app) => internalIds.includes(app.name))); + } + + setAppsLoaded(true) + }) + .catch((error) => { + console.log("App loading error: " + error.toString()); + setAppsLoaded(true) + //alert.error("App loading error: "+error.toString()); + }); + }; + + // Searhc by username, userId, workflow, appId should all work + const getUserProfile = (username, rerun) => { + fetch(`${globalUrl}/api/v1/users/creators/${username}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } + + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + console.log("Found creator: ", responseJson) + setCreatorProfile(responseJson) + } else { + console.log("Couldn't find the creator profile (rerun?): ", responseJson, rerun) + // If the current user is any of the Shuffle Creators + // AND the workflow doesn't have an owner: allow editing. + // else: Allow suggestions? + //console.log("User: ", userdata) + //if (rerun !== true) { + // getUserProfile(userdata.id, true) + //} + } + }) + .catch((error) => { + console.log("Get userprofile error: ", error); + }) + } + + const getFiles = () => { + fetch(globalUrl + "/api/v1/files", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + //console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.files !== undefined && responseJson.files !== null) { + setFiles(responseJson); + } else { + setFiles({ + "namespaces": [ + "default" + ] + }); + } + + if ( + responseJson.namespaces !== undefined && + responseJson.namespaces !== null + ) { + //setFileNamespaces(responseJson.namespaces); + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Error loading files: ", error) + }); + }; + + const getWorkflow = (workflow_id, sourcenode) => { + fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + + if (response.status >= 500) { + alert.info("Something went wrong while loading the workflow. Please reload.") + } else { + alert.info("You don't access to this workflow or loading failed.") + window.location.pathname = "/workflows"; + } + } + + // Read text from stream + //return response.text(); + return response.json(); + }) + .then((responseJson) => { + // Load as JSON + //console.log("Got workflow TXT: ", responseText) + //const responseJson = JSON.parse(responseText) + //console.log("Got workflow JSON: ", responseJson) + + // Not sure why this is necessary. + if (responseJson.isValid === undefined) { + responseJson.isValid = true; + } + + if (responseJson.errors === undefined) { + responseJson.errors = []; + } + + if (responseJson.actions === undefined || responseJson.actions === null) { + responseJson.actions = []; + } + + if (responseJson.triggers === undefined || responseJson.triggers === null) { + responseJson.triggers = []; + } + + fetchRecommendations(responseJson) + + if (responseJson.public) { + //alert.info("This workflow is public. Save the workflow to use it in your organization."); + + setAppAuthentication([]) + console.log("RESP: ", responseJson) + if (Object.getOwnPropertyNames(creatorProfile).length === 0) { + //getUserProfile("frikky") + getUserProfile(responseJson.id, false) + } + + //{appGroup.map((data, index) => { + //const [appGroup, setAppGroup] = React.useState([]); + var appsFound = [] + for (let actionkey in responseJson.actions) { + const parsedAction = responseJson.actions[actionkey] + if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") { + continue + } + if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ + appsFound.push(parsedAction) } } - } - } - } - */ - // FIXME - check if they have value before overriding like this for no reason. - // Would save a lot of time (400~ ms -> 30ms) - //console.log("ACTION: ", selectedAction) - //console.log("APP: ", selectedApp) - setSelectedAction({}) - setSelectedApp({}) - setSelectedTrigger({}) - //setSelectedEdge({}) + setAppGroup(appsFound) - // setSelectedTriggerIndex(-1) - //setSelectedActionEnvironment({}) - setSelectedEdge({}) - //setTriggerAuthentication({}) - //setSelectedTriggerIndex(-1) - //setTriggerFolders([]) + appsFound = [] + for (let triggerkey in responseJson.triggers) { + const parsedAction = responseJson.triggers[triggerkey] + if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ + appsFound.push(parsedAction) + } + } - // Can be used for right side view - setRightSideBarOpen(false) - setScrollConfig({ - top: 0, - left: 0, - selected: "", - }) - console.timeEnd("UNSELECT") - } + setTriggerGroup(appsFound) + } else { + getAppAuthentication(); + getEnvironments(); + getWorkflowExecution(props.match.params.key, ""); + getAvailableWorkflows(-1); + getSettings(); + getFiles() + } - const onEdgeSelect = (event) => { - setRightSideBarOpen(true) - setLastSaved(false) + // Appends SUBFLOWS. Does NOT run during normal grabbing of workflows. + if (sourcenode.id !== undefined) { + console.log("WORKFLOW: ", responseJson); - /* - // Used to not be able to edit trigger-based branches. - const triggercheck = workflow.triggers.find(trigger => trigger.id === event.target.data()["source"]) - if (triggercheck === undefined) { - */ - //console.log(event.target.data()) - if (event.target.data().decorator) { - alert.info("This edge can't be edited.") - } else { - setSelectedEdgeIndex(workflow.branches.findIndex(data => data.id === event.target.data()["id"])) - setSelectedEdge(event.target.data()) - } + var nodefound = false; + var target = sourcenode.parameters.find((item) => item.name === "startnode"); + console.log("Got rightclick target: ", target) + if (target === undefined || target === null) { + target = { + "name": "startnode", + "value": responseJson.start + } + } + + console.log(sourcenode.parameters); + console.log(target); + const target_id = target === undefined ? "" : target.value; + const actions = responseJson.actions.map((action) => { + const node = { + group: "nodes", + }; - /* - } else { - //alert.info("Can't edit branches from triggers") - console.log("IN HERE: !", triggercheck) - } - */ + // Set it dynamically? + node.position = { + x: sourcenode.position.x + action.position.x, + y: sourcenode.position.y + action.position.y, + }; - setSelectedAction({}) - setSelectedTrigger({}) - } + node.data = action; - // Comparing locations between nodes and setting views - var styledElements = [] - var originalLocation = { - x: 0, - y: 0, - } + node.data.canConnect = false + node.data.is_valid = true + node.data.isValid = true + node.data._id = action["id"]; + node.data.type = "ACTION"; + node.data.source_workflow = responseJson.id; + if (action.id === target_id) { + nodefound = true; + } + + if (responseJson.public) { + node.data.is_valid = true + node.is_valid = true + } + + var example = ""; + if ( + action.example !== undefined && + action.example !== null && + action.example.length > 0 + ) { + example = action.example; + } + + node.data.example = example; + return node; + }); + + var triggers = responseJson.triggers.map((trigger) => { + const node = {}; + + console.log("Only add workflow: ", trigger.app_name) + if (trigger.app_name !== "Shuffle Workflow" && trigger.app_name !== "User Input") { + return null + } + + node.position = trigger.position; + node.data = trigger; + + node.data.canConnect = false + node.data.id = trigger["id"]; + node.data._id = trigger["id"]; + node.data.type = "TRIGGER"; + + return node; + }); + + triggers = triggers.filter((trigger) => trigger !== null); + const insertedNodes = [].concat(actions, triggers); + var edges = responseJson.branches.map((branch, index) => { + const edge = {}; + var conditions = responseJson.branches[index].conditions; + if (conditions === undefined || conditions === null) { + conditions = []; + } + + var label = ""; + if (conditions.length === 1) { + label = conditions.length + " condition"; + } else if (conditions.length > 1) { + label = conditions.length + " conditions"; + } + + const sourceFound = insertedNodes.findIndex( + (action) => action.data.id === branch.source_id + ); + if (sourceFound < 0) { + return null; + } + + const destinationFound = insertedNodes.findIndex( + (action) => action.data.id === branch.destination_id + ); + if (destinationFound < 0) { + return null; + } - const onCtxTap = (event) => { - const nodedata = event.target.data() - console.log(nodedata) - if (nodedata.type === "TRIGGER" && nodedata.app_name === "Shuffle Workflow") { - if (nodedata.parameters === null) { - alert.error("Set a workflow first") - return - } + edge.data = { + id: branch.id, + _id: branch.id, + source: branch.source_id, + target: branch.destination_id, + label: label, + conditions: conditions, + hasErrors: branch.has_errors, + decorator: false, + source_workflow: responseJson.id, + }; - const workflow_id = nodedata.parameters.find(param => param.name === "workflow") - if (workflow.id === workflow_id.valu) { - return - } + if (responseJson.public) { + edge.data.is_valid = true + edge.is_valid = true + } - cy.animation({ - zoom: 0, - center: { - eles: event.target, - }, - }) + return edge; + }); + + edges = edges.filter((edge) => edge !== null); + cy.removeListener("add"); + cy.add(insertedNodes) + cy.add(edges); + + if (nodefound === true) { + const newId = uuidv4(); + cy.add({ + group: "edges", + data: { + id: newId, + _id: newId, + source: sourcenode.id, + target: target_id, + label: "Subflow", + decorator: true, + source_workflow: responseJson.id, + }, + }); + } + + cy.fit(null, 100); + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + } else { + setWorkflow(responseJson); + setWorkflowDone(true); + + // Add error checks + if (!responseJson.public) { + if ( + !responseJson.previously_saved || + !responseJson.is_valid || + responseJson.errors !== undefined || + responseJson.errors !== null || + responseJson.errors !== // what + responseJson.errors.length > 0 + ) { + console.log("Setting configure Modal to open") + setConfigureWorkflowModalOpen(true); + } + } + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get workflows error: ", error.toString()); + }); + }; + + const onUnselect = (event) => { + const nodedata = event.target.data(); + console.log("UNSELECT: ", nodedata); + //if (nodedata.type === "ACTION") { + // setLastSelected(nodedata) + //} + // + + // Wait for new node to possibly be selected + //setTimeout(() => { + const typeIds = cy.elements('node:selected').jsons(); + for (var idkey in typeIds) { + const item = typeIds[idkey] + if (item.data.isButton === true) { + //console.log("Reselect old node & return - or just return?") + + if (item.data.buttonType === "delete" && item.data.attachedTo === nodedata.id) { + //console.log("delete of same node!") + } + return + } + } + + // Unselecting all + //cy.elements().unselect() + + //if (nodedata.app_name === undefined && nodedata.source === undefined) { + // return; + //} + //event.target.removeClass("selected"); + // + + + + + + //// If button is clicked, select current node + + // Attempt at rewrite of name in other actions in following nodes. + // Should probably be done in the onBlur for the textfield instead + /* + if (event.target.data().type === "ACTION") { + const nodeaction = event.target.data() + const curaction = workflow.actions.find(a => a.id === nodeaction.id) + console.log("workflowaction: ", curaction) + console.log("nodeaction: ", nodeaction) + if (nodeaction.label !== curaction.label) { + console.log("BEACH!") + + var params = [] + const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_") + for (var actionkey in workflow.actions) { + if (workflow.actions[actionkey].id === curaction.id) { + continue + } + + for (var paramkey in workflow.actions[actionkey].parameters) { + const param = workflow.actions[actionkey].parameters[paramkey] + if (param.value === null || param.value === undefined || !param.value.includes("$")) { + continue + } + + const innername = param.value.toLowerCase().replace(" ", "_") + if (innername.includes(fixedName)) { + //workflow.actions[actionkey].parameters[paramkey].replace( + //console.log("FOUND!: ", innername) + } + } + } + } + } + */ + + + // FIXME - check if they have value before overriding like this for no reason. + // Would save a lot of time (400~ ms -> 30ms) + //console.log("ACTION: ", selectedAction) + //console.log("APP: ", selectedApp) + + //setSubworkflow({}) + ReactDOM.unstable_batchedUpdates(() => { + setSelectedAction({}); + setSelectedApp({}); + setSelectedTrigger({}); + setSelectedComment({}) + setSelectedEdge({}); + + setSelectedEdge({}) + setSelectedActionEnvironment({}) + setTriggerAuthentication({}) + setSelectedTriggerIndex(-1) + setTriggerFolders([]) + setLocalFirstrequest(true) + + // Can be used for right side view + setRightSideBarOpen(false); + setScrollConfig({ + top: 0, + left: 0, + selected: "", + }); + //console.timeEnd("UNSELECT"); + }) + + sendStreamRequest({ + "item": "node", + "type": "unselect", + "userid": userdata.id, + }) + //}, 150) + }; + + const onEdgeSelect = (event) => { + ReactDOM.unstable_batchedUpdates(() => { + setRightSideBarOpen(true); + setLastSaved(false); + + /* + // Used to not be able to edit trigger-based branches. + const triggercheck = workflow.triggers.find(trigger => trigger.id === event.target.data()["source"]) + if (triggercheck === undefined) { + */ + if ( + event.target.data("type") !== "COMMENT" && + event.target.data().decorator + ) { + alert.info("This edge can't be edited."); + } else { + //console.log("DATA: ", event.target.data()) + const destinationId = event.target.data("target"); + //console.log("DATA: ", event.target.data()) + const curaction = workflow.actions.find((a) => a.id === destinationId); + //console.log("ACTION: ", curaction) + if (curaction !== undefined && curaction !== null) { + if ( + curaction.app_name === "Shuffle Tools" && + curaction.name === "router" + ) { + alert.info("Router action can't have incoming conditions"); + event.target.unselect(); + return; + } + } + + setSelectedEdgeIndex( + workflow.branches.findIndex( + (data) => data.id === event.target.data()["id"] + ) + ); + setSelectedEdge(event.target.data()); + } + + setSelectedAction({}); + setSelectedTrigger({}); + }) + }; + + // Comparing locations between nodes and setting views + var styledElements = []; + var originalLocation = { + x: 0, + y: 0, + }; + + const onCtxTap = (event) => { + const nodedata = event.target.data(); + console.log(nodedata); + if (nodedata.type === "TRIGGER" && (nodedata.app_name === "Shuffle Workflow" || nodedata.app_name === "User Input")) { + + if (nodedata.parameters === null) { + alert.error("Set a workflow first"); + return; + } + + if (nodedata.parameters === undefined) { + return + } + + const workflow_id = nodedata.parameters.find((param) => param.name === "workflow" || param.name === "subflow"); + + if (workflow.id === workflow_id.valu) { + return; + } + + cy.animation({ + zoom: 0, + center: { + eles: event.target, + }, + }) .play() .promise() - .then( () => { - console.log("DONE: ", workflow_id) - //cy.elements().remove() - getWorkflow(workflow_id.value, nodedata) - cy.fit(null, 50) - //props.match.params.key - //cy.animation({ - // zoom: 500, - // center: { - // eles: cy.nodes(), - // }, - //}) - //.play() - //.promise() - }) + .then(() => { + console.log("DONE: ", workflow_id); + getWorkflow(workflow_id.value, nodedata); + cy.fit(null, 50); + }); + } + }; - //const animationDuration = 150 - //style: { - //maxZoom={2.00} - //cy.animate({ - // fit: 100, - //}, { - // duration: 500, - //}) - } - } - //cy.on('cxttap', "node", (e) => onCtxTap(e)) + const onNodeDragStop = (event, selectedAction) => { + const nodedata = event.target.data(); + if (nodedata.id === selectedAction.id) { + return; + } - var hiddenNodes = [] - const onNodeDragStop = (event, selectedAction) => { - const nodedata = event.target.data() - //console.log("IN NODE DRAG STOP: ", nodedata) - if (nodedata.id === selectedAction.id) { - return - } + if (nodedata.finished === false) { + return; + } - if (nodedata.finished === false) { - return - } + const connected = event.target.connectedEdges().jsons() + if (connected.length > 0 && connected !== undefined) { + for (let connectkey in connected) { + const edge = connected[connectkey] + //console.log("EDGE:", edge) - //console.log("Drag: ", nodedata) - //return + //const edge = edgeBase.json() - //console.log("DRAGGED NODE: ", nodedata) - //console.log("TARGET NODE: ", selectedAction) - if (styledElements.length === 1) { - console.log("Should reset location and autofill: ", styledElements, selectedAction) - //event.target.position = originalLocation - //curworkflowTrigger.position = cyelements[key].position() - if (originalLocation.x !== 0 || originalLocation.y !== 0) { - const currentnode = cy.getElementById(nodedata.id) - if (currentnode !== null && currentnode !== undefined) { - //currentnode.position = originalLocation - currentnode.position("x", originalLocation.x) - currentnode.position("y", originalLocation.y) + const sourcenode = cy.getElementById(edge.data.source) + const destinationnode = cy.getElementById(edge.data.target) + if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { + continue } - originalLocation = {x: 0, y: 0} + const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) + const currentedge = cy.getElementById(edge.data.id) + if (currentedge !== undefined && currentedge !== null) { + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) + } + } + } + + if (styledElements.length === 1) { + console.log( + "Should reset location and autofill: ", + styledElements, + selectedAction + ); + if (originalLocation.x !== 0 || originalLocation.y !== 0) { + const currentnode = cy.getElementById(nodedata.id); + if (currentnode !== null && currentnode !== undefined) { + currentnode.position("x", originalLocation.x); + currentnode.position("y", originalLocation.y); + } + + originalLocation = { x: 0, y: 0 }; + } + + const curElement = document.getElementById(styledElements[0]); + if (curElement !== null && curElement !== undefined) { + curElement.style.border = curElement.style.original_border; + var newValue = "$" + nodedata.label.toLowerCase().replaceAll(" ", "_"); + if (nodedata.type === "TRIGGER") { + if (nodedata.trigger_type === "WEBHOOK" || nodedata.trigger_type === "SCHEDULE" || nodedata.trigger_type === "EMAIL") { + var newValue = "$exec" + } + } + var paramname = ""; + var idnumber = -1; + if (curElement.id.startsWith("rightside_field_")) { + console.log("FOUND FIELD WITH NUMBER: ", curElement.id); + + + // Find exact position to put the text + + const idsplit = curElement.id.split("_"); + console.log(idsplit); + if (idsplit.length === 3 && !isNaN(idsplit[2])) { + console.log("ADDING TO PARAM ", idsplit[2]); + + selectedAction.parameters[idsplit[2]].value += newValue; + paramname = selectedAction.parameters[idsplit[2]].name; + idnumber = idsplit[2]; + } + } + + if (idnumber >= 0 && paramname.length > 0) { + const exampledata = GetExampleResult(nodedata); + const parsedname = paramname + .toLowerCase() + .trim() + .replaceAll("_", " "); + + const foundresult = GetParamMatch(parsedname, exampledata, ""); + if (foundresult.length > 0) { + console.log("FOUND RESULT: ", paramname, foundresult); + newValue = `${newValue}${foundresult}`; + } + + selectedAction.parameters[idnumber].value = newValue; + } + + curElement.value = newValue; + } + } + + if ( + nodedata.app_name !== undefined && + ((nodedata.app_name !== "Shuffle Tools" && + nodedata.app_name !== "Testing" && + nodedata.app_name !== "Shuffle Workflow" && + nodedata.app_name !== "User Input") || + nodedata.isStartNode) + ) { + const allNodes = cy.nodes().jsons(); + var found = false; + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if ( + currentNode.data.attachedTo === nodedata.id && + currentNode.data.isDescriptor + ) { + found = true; + console.log("FOUND THE NODE!"); + break; + } } - const curElement = document.getElementById(styledElements[0]) - if (curElement !== null && curElement !== undefined) { - //console.log("ELE: ", curElement) - curElement.style.border = curElement.style.original_border - var newValue = "$"+nodedata.label.toLowerCase().replaceAll(" ", "_") - var paramname = "" - var idnumber = -1 - if (curElement.id.startsWith("rightside_field_")) { - console.log("FOUND FIELD WITH NUMBER: ", curElement.id) - const idsplit = curElement.id.split("_") - console.log(idsplit) - if (idsplit.length === 3 && !isNaN(idsplit[2])) { - console.log("ADDING TO PARAM ", idsplit[2]) - console.log("PARAM: ", selectedAction) + 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: { + x: event.target.position().x + 44, + y: event.target.position().y + 44, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + isTrigger: true, + label: `${matchingExecutions.length}`, + attachedTo: nodedata.id, + imageColor: color, + hasExecutions: true, + }, + }; + + cy.add(decoratorNode) + } + } else { + // Readding the icon after moving the node + if (!found) { + const iconInfo = GetIconInfo(nodedata); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const offset = nodedata.isStartNode ? 36 : 44; + const decoratorNode = { + position: { + x: event.target.position().x + offset, + y: event.target.position().y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: nodedata.id, + }, + }; + + cy.add(decoratorNode).unselectify(); + } else { + console.log("Node already exists - don't add descriptor node"); + } + } + } + + originalLocation = { + x: 0, + y: 0, + }; + + sendStreamRequest({ + "item": "node", + "type": "move", + "id": nodedata.id, + "location": { + "x": event.target.position("x"), + "y": event.target.position("y"), + } + }) + }; + + const onNodeDrag = (event, selectedAction) => { + const nodedata = event.target.data(); + if (nodedata.finished === false) { + console.log("NOT FINISHED - ADD EXAMPLE BRANCHES TO CLOSEST!!") + return + } + + if (nodedata.app_name !== undefined) { + const allNodes = cy.nodes().jsons(); + for (var nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if (currentNode.data.attachedTo === nodedata.id) { + cy.getElementById(currentNode.data.id).remove(); + } + + // Calculate location + //currentNode.position.x > + //if (nodedata.position.x > 0 && nodedata.position.y > 0) { + // console.log("Positive both") + //} + + //console.log(currentNode.position) + //console.log(nodedata.position) + } + } else { + //console.log("No appid? ", nodedata) + } + + if (nodedata.buttonType === "edgehandler") { + console.log("Enable edgehandler!") + console.log("Find parent: ", nodedata.attachedTo) + const parentNode = cy.getElementById(nodedata.attachedTo); + if (parentNode !== null && parentNode !== undefined) { + console.log("Start parentnode tracking!") + //cy.edgehandles().start(parentNode) + } + } + + if (nodedata.id === selectedAction.id) { + return; + } + + + + /* + // Tried looking for the closest node by position. aStar path not working entirely. + console.log("NODE: ", event.target) + const closestNode = cy.elements().aStar({ + root: nodedata.id, + goal: 'node', + directed: false, + }) + + if (closestNode.found) { + console.log("No closest node found for: ", nodedata.id) + } else { + console.log("Closest: ", closestNode) + } + */ + + if ( + originalLocation.x === 0 && + originalLocation.y === 0 && + nodedata.position !== undefined + ) { + originalLocation.x = nodedata.position.x; + originalLocation.y = nodedata.position.y; + } + + // Part of autocomplete. Styles elements in frontend to indicate + // what and where we may input data for the user. + const onMouseUpdate = (e) => { + const x = e.pageX; + const y = e.pageY; + + const elementMouseIsOver = document.elementFromPoint(x, y); + if (elementMouseIsOver !== undefined && elementMouseIsOver !== null) { + // Color for #f85a3e translated to rgb + const newBorder = "3px solid rgb(248, 90, 62)"; + if ( + elementMouseIsOver.style.border !== newBorder && + elementMouseIsOver.id.includes("rightside") + ) { + if (elementMouseIsOver.style.border !== undefined) { + elementMouseIsOver.style.original_border = + elementMouseIsOver.style.border; + } else { + elementMouseIsOver.style.original_border = ""; + } + + elementMouseIsOver.style.border = newBorder; + console.log("STYLED: ", styledElements); + for (var styledElementsKey in styledElements) { + const curElement = document.getElementById(styledElements[styledElementsKey]); + if (curElement !== null && curElement !== undefined) { + curElement.style.border = curElement.style.original_border; + } + } + + styledElements = []; + styledElements.push(elementMouseIsOver.id); + } else if ( + elementMouseIsOver.id === "cytoscape_view" || + elementMouseIsOver.id === "" + ) { + for (var index in styledElements) { + const curElement = document.getElementById(styledElements[index]); + if (curElement !== null && curElement !== undefined) { + curElement.style.border = curElement.style.original_border; + } + } + + styledElements = []; + } + } + + // Ensure it only happens once + document.removeEventListener("mousemove", onMouseUpdate, false); + }; + + document.addEventListener("mousemove", onMouseUpdate, false); + }; + + + useBeforeunload(() => { + if (!lastSaved) { + return unloadText; + } else { + if (workflow.public === false) { + //document.removeEventListener("mousemove", onMouseUpdate, true); + document.removeEventListener("keydown", handleKeyDown, true); + document.removeEventListener("paste", handlePaste, true); + } + } + }); + + // Nodeselectbatching: + // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once + // onNodeClick + const onNodeSelect = (event, newAppAuth) => { + // Forces all states to update at the same time, + // Otherwise everything is SUPER slow + + ReactDOM.unstable_batchedUpdates(() => { + const data = event.target.data(); + if (data.isButton) { + if (data.buttonType === "suggestion") { + if (cy === undefined) { + console.log("Cy not defined yet") + return + } + + // Inject HTML at a fixed location + //const newHtml = "

    Do you want to add this suggestion?

    " + + // Find mouse cursor position on screen + console.log("Suggestion html to be added at location: ", event) + /* + const position = { + "top": cy.pan().y, + "left": cy.pan().x, + } + */ + + + const position = event.target.renderedPosition(); + const container = cy.container(); + const offset = { + left: container.offsetLeft, + top: container.offsetTop + }; - selectedAction.parameters[idsplit[2]].value = newValue - paramname = selectedAction.parameters[idsplit[2]].name - idnumber = idsplit[2] - } - } + // Calculate the actual screen position for the box + const screenPosition = { + left: position.x + offset.left - 150, + top: position.y + offset.top, + }; + + // Log the position to the console + console.log('Node screen position:', screenPosition); - if (idnumber >= 0 && paramname.length > 0) { - const exampledata = GetExampleResult(nodedata) - const parsedname = paramname.toLowerCase().trim().replaceAll("_", " ") - - const foundresult = GetParamMatch(parsedname, exampledata, "") - if (foundresult.length > 0) { - console.log("FOUND RESULT: ", paramname, foundresult) - newValue = `${newValue}${foundresult}` - } - - selectedAction.parameters[idnumber].value = newValue - } - - curElement.value = newValue - } - } - - const skipnames = [] - if (nodedata.app_name !== undefined && (( - nodedata.app_name !== "Shuffle Tools" && - nodedata.app_name !== "Testing" && - nodedata.app_name !== "Shuffle Workflow" && - nodedata.app_name !== "User Input" && - nodedata.app_name !== "Webhook" && - nodedata.app_name !== "Schedule" && - nodedata.app_name !== "Email") || nodedata.isStartNode) - ) { - const allNodes = cy.nodes().jsons() - var found = false - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) { - found = true - console.log("FOUND THE NODE!") - break - } - } - - // Readding the icon after moving the node - if (!found) { - //console.log("Node wasn't found") - const iconInfo = GetIconInfo(nodedata) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - const offset = nodedata.isStartNode ? 36 : 44 - //console.log(event.target.position()) - const decoratorNode = { - position: { - x: event.target.position().x+offset, - y: event.target.position().y+offset, - }, - locked: true, - data: { - "isDescriptor": true, - "isValid": true, - "is_valid": true, - "label": "", - "image": svgpin_Url, - "imageColor": iconInfo.iconBackgroundColor, - "attachedTo": nodedata.id, - }, - } - - cy.add(decoratorNode).unselectify() - } else { - console.log("Node already exists - don't add descriptor node") - } - } else { - //console.log("Shouldnt re-add info? ") - } - - originalLocation = { - x: 0, - y: 0, - } - } - - const onNodeDrag = (event, selectedAction) => { - //if (Object.getOwnPropertyNames(selectedAction).length === 0) { - // return - //} - const nodedata = event.target.data() - //console.log("Dragging: ", nodedata) - if (nodedata.finished === false) { - return - } - - //console.log("Dragging node!!") - if (nodedata.app_name == "Shuffle Tools" || nodedata.app_name == "Testing") { - //console.log("NODE: ", - //selector: `node[app_name="Shuffle Tools"]`, - //console.log(event.target) - - // 1. Find location of node - // 2. Check if it's within view of another node (inside) - // 3. If it is, then hide text - } - - if (nodedata.app_name !== undefined) { - //console.log("Trying to remove friendly nodes") - const allNodes = cy.nodes().jsons() - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.attachedTo === nodedata.id) { - cy.getElementById(currentNode.data.id).remove() - } - } - } else { - console.log("No appid? ", nodedata) - } - - if (nodedata.id === selectedAction.id) { - return - } - - if (originalLocation.x === 0 && originalLocation.y === 0 && nodedata.position !== undefined) { - //console.log("Updating location!: ", nodedata) - originalLocation.x = nodedata.position.x - originalLocation.y = nodedata.position.y - } - - // Part of autocomplete. Styles elements in frontend to indicate - // what and where we may input data for the user. - const onMouseUpdate = (e) => { - const x = e.pageX; - const y = e.pageY; - - const elementMouseIsOver = document.elementFromPoint(x, y) - if (elementMouseIsOver !== undefined && elementMouseIsOver !== null) { - // Color for #f85a3e translated to rgb - const newBorder = "3px solid rgb(248, 90, 62)" - if (elementMouseIsOver.style.border != newBorder && elementMouseIsOver.id.includes("rightside")) { - //console.log(elementMouseIsOver.style.border) - if (elementMouseIsOver.style.border !== undefined) { - elementMouseIsOver.style.original_border = elementMouseIsOver.style.border - } else { - elementMouseIsOver.style.original_border = "" + const newbox = { + "position": screenPosition, + "node_position": event.target.position(), + "open": true, + "attachedTo": data.attachedTo, } - elementMouseIsOver.style.border = newBorder - console.log("STYLED: ", styledElements) - for (var key in styledElements) { - const curElement = document.getElementById(styledElements[key]) - if (curElement !== null && curElement !== undefined) { - curElement.style.border = curElement.style.original_border + console.log("Rendered position: ", newbox.node_position) + + setSuggestionBox(newbox) + + // Unselect + event.target.unselect(); + + } else if (data.buttonType === "delete") { + const parentNode = cy.getElementById(data.attachedTo); + if (parentNode !== null && parentNode !== undefined) { + removeNode(data.attachedTo) + //parentNode.remove() + } + + return + } else if (data.buttonType === "set_startnode" && data.type !== "TRIGGER") { + const parentNode = cy.getElementById(data.attachedTo); + if (parentNode !== null && parentNode !== undefined) { + var oldstartnode = cy.getElementById(workflow.start); + if ( + oldstartnode !== null && + oldstartnode !== undefined && + oldstartnode.length > 0 + ) { + try { + oldstartnode[0].data("isStartNode", false); + } catch (e) { + console.log("Startnode error: ", e); + } + } + + workflow.start = parentNode.data("id"); + setLastSaved(false); + parentNode.data("isStartNode", true); + } + + //event.target.unselect(); + setRightSideBarOpen(true); + return + + } else if (data.buttonType === "copy") { + console.log("COPY!"); + + // 1. Find parent + // 2. Find branches for parent + // 3. Make a new node that's moved a little bit + const parentNode = cy.getElementById(data.attachedTo); + if (parentNode !== null && parentNode !== undefined) { + var newNodeData = JSON.parse(JSON.stringify(parentNode.data())); + newNodeData.id = uuidv4(); + if (newNodeData.position !== undefined) { + newNodeData.position = { + x: newNodeData.position.x + 100, + y: newNodeData.position.y + 100, + }; + } + + newNodeData.isStartNode = false; + newNodeData.errors = []; + newNodeData.is_valid = true; + newNodeData.isValid = true; + newNodeData.label = parentNode.data("label") + "_copy"; + + cy.add({ + group: "nodes", + data: newNodeData, + position: newNodeData.position, + }); + + // Readding the icon after moving the node + if ( + newNodeData.app_name !== "Testing" || + newNodeData.app_name !== "Shuffle Workflow" + ) { + } else { + const iconInfo = GetIconInfo(newNodeData); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const offset = newNodeData.isStartNode ? 36 : 44; + const decoratorNode = { + position: { + x: newNodeData.position.x + offset, + y: newNodeData.position.y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: newNodeData.id, + }, + }; + + cy.add(decoratorNode).unselectify(); + } + + workflow.actions.push(newNodeData); + + const sourcebranches = workflow.branches.filter( + (foundbranch) => foundbranch.source_id === parentNode.data("id") + ); + const destinationbranches = workflow.branches.filter( + (foundbranch) => + foundbranch.destination_id === parentNode.data("id") + ); + + for (var sourceBranchesKey in sourcebranches) { + var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); + newbranch.id = uuidv4(); + newbranch.source_id = newNodeData.id; + + newbranch._id = newbranch.id; + newbranch.source = newbranch.source_id; + newbranch.target = newbranch.destination_id; + cy.add({ + group: "edges", + data: newbranch, + }); + } + + for (var destinationBranchesKey in destinationbranches) { + var newbranch = JSON.parse( + JSON.stringify(destinationbranches[destinationBranchesKey]) + ); + newbranch.id = uuidv4(); + newbranch.destination_id = newNodeData.id; + + newbranch._id = newbranch.id; + newbranch.source = newbranch.source_id; + newbranch.target = newbranch.destination_id; + cy.add({ + group: "edges", + data: newbranch, + }); + } + + //event.target.unselect(); + return + } + } + + return; + } else if (data.isDescriptor) { + 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; + } + + if (data.type === undefined) { + console.log("No type, automatically setting to action"); + data.type = "ACTION" + } + + if (data.type === "ACTION") { + setSelectedComment({}) + //var curaction = JSON.parse(JSON.stringify(data)) + // FIXME: Trust it to just work? + //event.target.data() + var curaction = workflow.actions.find((a) => a.id === data.id) + if (!curaction || curaction === undefined) { + if (data.id !== undefined && data.app_name !== undefined) { + workflow.actions.push(data) + setWorkflow(workflow) + curaction = data + } else { + if (workflow.public !== true) { + alert.error("Action not found. Please remake it."); + } + + event.target.remove(); + return; + } + } + + //var newapps = JSON.parse(JSON.stringify(apps)) + var newapps = apps + if (apps === null || apps === undefined || apps.length === 0) { + newapps = filteredApps + } + + // Check ID first, then names etc + // That way it always selects the right IF it exists + var curapp = newapps.find((a) => + a.id === curaction.app_id + ) + + if (curapp === undefined || curapp === null) { + console.log("Couldn't find ID - checking with name & version") + + curapp = newapps.find((a) => + a.name === curaction.app_name && + (a.app_version === curaction.app_version || + (a.loop_versions !== null && + a.loop_versions.includes(curaction.app_version))) + ) + } + + if (curaction.template === true && curaction.name !== undefined) { + //newapps. + const parsedname = curaction.name.replaceAll(" ", "_").toLowerCase() + console.log("FIND AN ACTION AMONG THE APPS THAT MATCHES NAME: ", parsedname) + + curaction.matching_actions = [] + for (var newAppskey in newapps) { + for (let actionsSubkey in newapps[newAppskey].actions) { + const tmpaction = newapps[newAppskey].actions[actionsSubkey] + if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { + console.log("MATCH!: ", newapps[newAppskey]) + curaction.matching_actions.push({ + "app_name": newapps[newAppskey].name, + "app_version": newapps[newAppskey].app_version, + "app_id": newapps[newAppskey].id, + "action": tmpaction, + "large_image": newapps[newAppskey].large_image, + "app_index": newAppskey, + "action_index": actionsSubkey, + }) + } } } - - styledElements = [] - styledElements.push(elementMouseIsOver.id) - } else if (elementMouseIsOver.id === "cytoscape_view" || elementMouseIsOver.id === "") { - for (var key in styledElements) { - const curElement = document.getElementById(styledElements[key]) - if (curElement !== null && curElement !== undefined) { - curElement.style.border = curElement.style.original_border - } - } - - styledElements = [] - } - } - - // Ensure it only happens once - document.removeEventListener('mousemove', onMouseUpdate, false) - } - - document.addEventListener('mousemove', onMouseUpdate, false); - - /* - event.target.animate({ - style: { - "border-width": "12px", - "border-opacity": ".7", - } - }, { - duration: animationDuration, - }) - event.target.animate({ - style: { - "border-width": "12px", - "border-opacity": ".7", - } - }, { - duration: animationDuration, - }) - */ - } - - // Nodeselectbatching: - // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once - const onNodeSelect = (event, newAppAuth) => { - const data = event.target.data() - //console.log("NEW NODE ID: ", data.id) - if (data.isButton) { - //console.log("BUTTON CLICKED: ", data) - if (data.buttonType === "delete") { - //console.log("DELETE!") - const parentNode = cy.getElementById(data.attachedTo) - if (parentNode !== null && parentNode !== undefined) { - parentNode.remove() } - //for (var key in allNodes) { - // const currentNode = allNodes[key] - // if (currentNode.data.attachedTo === data.attachedTo) { - // cy.getElementById(currentNode.data.id).remove() - // } - //} - } else if (data.buttonType === "set_startnode" && data.type !== "TRIGGER") { - const parentNode = cy.getElementById(data.attachedTo) - if (parentNode !== null && parentNode !== undefined) { - var oldstartnode = cy.getElementById(workflow.start) - if (oldstartnode !== null && oldstartnode !== undefined && oldstartnode.length > 0) { - try { - oldstartnode[0].data("isStartNode", false) - } catch (e) { - console.log("Startnode error: ", e) - } - } + if (!curapp || curapp === undefined) { + console.log("APPS - couldn't find it: ", newapps) + //alert.error(`App ${curaction.app_name}:${curaction.app_version} not found. Is it activated?`); - workflow.start = parentNode.data('id') - parentNode.data('isStartNode', true) - } + const tmpapp = { + name: curaction.app_name, + app_name: curaction.app_name, + app_version: curaction.app_version, + id: curaction.app_id, + actions: [curaction], + }; - } else if (data.buttonType === "copy") { - console.log("COPY!") - // 1. Find parent - // 2. Find branches for parent - // 3. Make a new node that's moved a little bit - const parentNode = cy.getElementById(data.attachedTo) - if (parentNode !== null && parentNode !== undefined) { - //parentNode.data() - var newNodeData = JSON.parse(JSON.stringify(parentNode.data())) - newNodeData.id = uuidv4.v4() - if (newNodeData.position !== undefined) { - newNodeData.position = { - "x": newNodeData.position.x+100, - "y": newNodeData.position.y+100, - } - } + setSelectedApp(tmpapp); + setSelectedAction(curaction); + } else { + //if (curapp.id !== curaction.id) { + // curaction.app_id = curapp.id + // //.valueOf() + //} + curaction.app_id = curapp.id - newNodeData.isStartNode = false - newNodeData.errors = [] - newNodeData.is_valid = true - newNodeData.isValid = true - newNodeData.label = parentNode.data("label")+"_copy" + setAuthenticationType( + curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? { + type: "oauth2", + redirect_uri: curapp.authentication.redirect_uri, + refresh_uri: curapp.authentication.refresh_uri, + token_uri: curapp.authentication.token_uri, + scope: curapp.authentication.scope, + client_id: curapp.authentication.client_id, + client_secret: curapp.authentication.client_secret, + } : { + type: "", + } + ) - cy.add({ - group: 'nodes', - data: newNodeData, - position: newNodeData.position, - }) + const requiresAuth = curapp.authentication.required; //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) + setRequiresAuthentication(requiresAuth); + if (curapp.authentication.required) { + //console.log("App requires auth.") + // Setup auth here :) + const authenticationOptions = []; + var findAuthId = ""; + if ( + curaction.authentication_id !== null && + curaction.authentication_id !== undefined && + curaction.authentication_id.length > 0 + ) { + findAuthId = curaction.authentication_id; + } - // Readding the icon after moving the node - //console.log("Node wasn't found") - if (newNodeData.app_name !== "Testing" || newNodeData.app_name !== "Shuffle Workflow") { - } else { - const iconInfo = GetIconInfo(newNodeData) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) + const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); + //console.log("FOUND AUTH OPTIONS: ", tmpAuth) - const offset = newNodeData.isStartNode ? 36 : 44 - //console.log(event.target.position()) - const decoratorNode = { - position: { - x: newNodeData.position.x+offset, - y: newNodeData.position.y+offset, - }, - locked: true, - data: { - "isDescriptor": true, - "isValid": true, - "is_valid": true, - "label": "", - "image": svgpin_Url, - "imageColor": iconInfo.iconBackgroundColor, - "attachedTo": newNodeData.id, - }, - } + const curappName = curapp.name.toLowerCase() + for (let tmpAuthKey in tmpAuth) { + var item = tmpAuth[tmpAuthKey]; - cy.add(decoratorNode).unselectify() - } + const newfields = {}; + if (item.app.name.toLowerCase() !== curappName) { + continue + } - workflow.actions.push(newNodeData) + // Makes list into key:value object + for (let fieldkey in item.fields) { + if (item.fields[fieldkey] === undefined) { + console.log("Problem with filterkey in Node select", fieldkey) + continue + } - const sourcebranches = workflow.branches.filter(foundbranch => foundbranch.source_id === parentNode.data("id")) - const destinationbranches = workflow.branches.filter(foundbranch => foundbranch.destination_id === parentNode.data("id")) + const filterkey = item.fields[fieldkey]["key"] + if (filterkey === null || filterkey === undefined) { + console.log("Problem with filterkey 2. Null or undefined 3") + continue + } - //for (var key in sourcebranches) { - // var newbranch = JSON.parse(JSON.stringify(sourcebranches[key])) - // newbranch.id = uuidv4() - // newbranch.source_id = newNodeData.id - // cy.add({ - // group: "edges", - // data: newbranch, - // }) - //} + newfields[filterkey] = item.fields[fieldkey]["value"]; + } - for (var key in sourcebranches) { - var newbranch = JSON.parse(JSON.stringify(sourcebranches[key])) - newbranch.id = uuidv4() - newbranch.source_id = newNodeData.id + item.fields = newfields; + if (item.app.name.toLowerCase() === curappName) { + authenticationOptions.push(item); + if (item.id === findAuthId) { + curaction.selectedAuthentication = item; + } + } + } - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) - } + curaction.authentication = authenticationOptions; + if ( + curaction.selectedAuthentication === null || + curaction.selectedAuthentication === undefined || + curaction.selectedAuthentication.length === "" + ) { + curaction.selectedAuthentication = {}; + } + } else { + curaction.authentication = []; + curaction.authentication_id = ""; + curaction.selectedAuthentication = {}; + } - for (var key in destinationbranches) { - var newbranch = JSON.parse(JSON.stringify(destinationbranches[key])) - newbranch.id = uuidv4() - newbranch.destination_id = newNodeData.id - - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) - } - } - } - - event.target.unselect() - return - } else if (data.isDescriptor) { - console.log("Can't select descriptor") - event.target.unselect() - return - } - - //const node = cy.getElementById(data.id) - //if (node.length > 0) { - // node.addClass('shuffle-hover-highlight') - //} - - //const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) - //console.log("APPAUTH: ", newAppAuth) - //console.log("BRANCHES: ", branch) - //console.log("CLICK: ", data) - - if (data.type === "ACTION") { - //setSelectedApp({}) - //setSelectedAction({}) - var curaction = workflow.actions.find(a => a.id === data.id) - //console.log("PARAMS: ", curaction.parameters[0]) - //console.log("INSIDE CURACTION: ", curaction) - if (!curaction || curaction === undefined) { - //event.target.unselect() - alert.error("Action not found. Please remake it.") - return - } - - - const curapp = apps.find(a => a.name === curaction.app_name && ((a.app_version === curaction.app_version || (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version))))) - //console.log("APP: ", curapp) - if (!curapp || curapp === undefined) { - alert.error(`App ${curaction.app_name}:${curaction.app_version} not found. Is it activated?`) - - const tmpapp = { - name: curaction.app_name, - app_name: curaction.app_name, - app_version: curaction.app_version, - id: curaction.app_id, - actions: [curaction], - } - - //console.log(tmpapp) - //console.log(curaction) - setSelectedApp(tmpapp) - //setSelectedAction(JSON.parse(JSON.stringify(curaction))) - setSelectedAction(curaction) - //return - } else { - //console.log("AUTHENTICATION: ", curapp.authentication) - //console.log(curapp.authentication) - setAuthenticationType(curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? - { - "type": "oauth2", - "redirect_uri": curapp.authentication.redirect_uri, - "refresh_uri": curapp.authentication.refresh_uri, - "token_uri": curapp.authentication.token_uri, - "scope": curapp.authentication.scope, - "client_id": curapp.authentication.client_id, - "client_secret": curapp.authentication.client_secret, - } : { - "type": "" - } - ) - - const requiresAuth = curapp.authentication.required //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) - //console.log("AUTHCHECK: ", requiresAuth) - setRequiresAuthentication(requiresAuth) - if (curapp.authentication.required) { - //console.log("App requires auth.") - // Setup auth here :) - const authenticationOptions = [] - var findAuthId = "" - if (curaction.authentication_id !== null && curaction.authentication_id !== undefined && curaction.authentication_id.length > 0) { - findAuthId = curaction.authentication_id - } - - var tmpAuth = JSON.parse(JSON.stringify(newAppAuth)) - //console.log("FOUND AUTH: ", tmpAuth) - - //console.log("Checking authentication: ", tmpAuth) - for (var key in tmpAuth) { - var item = tmpAuth[key] - - const newfields = {} - for (var filterkey in item.fields) { - newfields[item.fields[filterkey].key] = item.fields[filterkey].value - } - - item.fields = newfields - if (item.app.name === curapp.name) { - authenticationOptions.push(item) - if (item.id === findAuthId) { - curaction.selectedAuthentication = item + if ( + curaction.parameters !== undefined && + curaction.parameters !== null && + curaction.parameters.length > 0 + ) { + for (var curActionParamKey in curaction.parameters) { + if ( + curaction.parameters[curActionParamKey].options !== undefined && + curaction.parameters[curActionParamKey].options !== null && + curaction.parameters[curActionParamKey].options.length > 0 && + curaction.parameters[curActionParamKey].value === "" + ) { + curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; + } + } + } else { + console.log("Should check APP if it has the same params as ACTION") + for (let actionKey in curapp.actions) { + const tmpaction = curapp.actions[actionKey] + if (tmpaction.name === curaction.name) { + console.log("Found action - needs change?", tmpaction) + if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { + curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) + } + break } } } - //console.log("OPTIONS: ", authenticationOptions) - curaction.authentication = authenticationOptions - //console.log("Authentication: ", authenticationOptions) - if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") { - curaction.selectedAuthentication = {} - } - } else { - curaction.authentication = [] - curaction.authentication_id = "" - curaction.selectedAuthentication = {} - } + //curaction["authentication"] = [] + //curaction["authentication_id"] = "" + // Fix parameters that are... Not ideal + //var paramnames = [] + //var newparams = [] + //for (let paramKey in curaction.parameters) { + // console.log("Name: ", curaction.parameters[paramKey].name) + // if (paramnames.includes(curaction.parameters[paramKey].name)) { + // continue + // } - //setSelectedAction(JSON.parse(JSON.stringify(curaction))) - //console.log("CURAPP: ", curapp, selectedApp) - //console.log("ACTION: ", curaction) - if (curaction.parameters !== undefined && curaction.parameters !== null && curaction.parameters.length > 0) { - //console.log("params: ", curaction.parameters) - for (var key in curaction.parameters) { - if (curaction.parameters[key].options !== undefined && curaction.parameters[key].options !== null && curaction.parameters[key].options.length > 0 && curaction.parameters[key].value == "") { - curaction.parameters[key].value = curaction.parameters[key].options[0] + // paramnames.push(curaction.parameters[paramKey].name) + // newparams.push(curaction.parameters[paramKey]) + //} + + //curaction.parameters = newparams + console.log("ACTION CLICK: ", curaction) + + setSelectedApp(curapp); + setSelectedAction(curaction); + + cy.removeListener("drag"); + cy.removeListener("free"); + cy.on("drag", "node", (e) => onNodeDrag(e, curaction)); + cy.on("free", "node", (e) => onNodeDragStop(e, curaction)); + } + + if (environments !== undefined && environments !== null && (typeof environments === "array" || typeof environments === "object")) { + var parsedenv = environments + //if (typeof environments === "object") { + // parsedenv = [environments] + //} + + const envs = parsedenv.find((a) => a.Name === curaction.environment); + var env = environments[defaultEnvironmentIndex] + if (envs !== undefined && envs !== null) { + env = envs + } + + setSelectedActionEnvironment(env); + } + } else if (data.type === "TRIGGER") { + setSelectedComment({}) + if (workflow.triggers === null) { + workflow.triggers = [] + } + + var trigger_index = workflow.triggers.findIndex( + (a) => a.id === data.id + ); + + //console.log("Trigger: ", data, trigger_index) + if (trigger_index === -1) { + workflow.triggers.push(data) + trigger_index = workflow.triggers.length - 1 + setWorkflow(workflow) + } + + //console.log("Trigger2: ", data, trigger_index) + //if (data.id !== undefined && data.app_name !== undefined) { + // //newapps.push(data) + // workflow.actions.push(data) + // curaction = data + //} else { + // alert.error("Action not found. Please remake it."); + // event.target.remove(); + // return; + //} + + if (data.app_name === "Shuffle Workflow" || data.app_name === "User Input") { + console.log("In Workflow loading for user input & subflow") + + getAvailableWorkflows(trigger_index); + getSettings(); + } else if (data.app_name === "Webhook") { + if (workflow.triggers[trigger_index].parameters !== undefined && workflow.triggers[trigger_index].parameters !== null && workflow.triggers[trigger_index].parameters.length > 0) { + console.log("Can set params here!") + workflow.triggers[trigger_index].parameters[0] = { + name: "url", + value: referenceUrl + "webhook_" + workflow.triggers[trigger_index].id, + }; + + if (workflow.triggers[trigger_index].parameters.length < 5) { + console.log("Adding to webhook params!") + workflow.triggers[trigger_index].parameters.push({ + name: "await_response", + value: "v1," + }) } - } + } + } + + setSelectedTriggerIndex(trigger_index); + setSelectedTrigger(data); + setSelectedActionEnvironment(data.env); + } else if (data.type === "COMMENT") { + setSelectedComment(data); + } else { + alert.error("Can't handle node type " + data.type); + return; + } + + setRightSideBarOpen(true); + setLastSaved(false); + setScrollConfig({ + top: 0, + left: 0, + selected: "", + }); + + console.log("DOne in the node update") + + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", + }); + sendStreamRequest({ + "item": "node", + "type": "select", + "id": data.id, + "userid": userdata.id, + "location": { + "x": event.target.position("x"), + "y": event.target.position("y"), + } + }) + + }) + } + + const activateApp = (appid, refresh) => { + fetch(`${globalUrl}/api/v1/apps/${appid}/activate`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Failed to activate") + } + + return response.json() + }) + .then((responseJson) => { + if (responseJson.success === false) { + alert.error("Failed to auto-activate the app. Go to /apps and activate it.") + } else { + if (refresh === true) { + //alert.success("App activated for your organization! Refresh the page to use the app.") + getApps() + } + } + }) + .catch(error => { + //alert.error(error.toString()) + console.log("Activate app error: ", error.toString()) + }); + } + + const GetExampleResult = (item) => { + var exampledata = item.example === undefined ? "" : item.example; + if (workflowExecutions.length > 0) { + // Look for the ID + for (let execkey in workflowExecutions) { + if ( + workflowExecutions[execkey].results === undefined || + workflowExecutions[execkey].results === null + ) { + continue; + } + + var foundResult = { result: "" }; + if (item.id === "exec") { + if ( + workflowExecutions[execkey].execution_argument !== undefined && + workflowExecutions[execkey].execution_argument !== null && + workflowExecutions[execkey].execution_argument.length > 0 + ) { + foundResult.result = workflowExecutions[execkey].execution_argument; + } else { + continue; + } + } else { + foundResult = workflowExecutions[execkey].results.find( + (result) => result.action.id === item.id + ); + if (foundResult === undefined) { + continue; + } + } + + foundResult.result = foundResult.result.trim(); + foundResult.result = foundResult.result.split(" None").join(' "None"'); + foundResult.result = foundResult.result.split(" False").join(" false"); + foundResult.result = foundResult.result.split(" True").join(" true"); + + var jsonvalid = true; + try { + if ( + !foundResult.result.includes("{") && + !foundResult.result.includes("[") + ) { + jsonvalid = false; + } + } catch (e) { + try { + foundResult.result = foundResult.result.split("'").join('"'); + if ( + !foundResult.result.includes("{") && + !foundResult.result.includes("[") + ) { + jsonvalid = false; + } + } catch (e) { + jsonvalid = false; + } + } + + // Finds the FIRST json only + if (jsonvalid) { + try { + exampledata = JSON.parse(foundResult.result) + } catch (e) { + console.log("Result: ", exampledata) + + } + + break; + } + } + } + + return exampledata; + }; + + const GetParamMatch = (paramname, exampledata, basekey) => { + if (typeof exampledata !== "object") { + return ""; + } + + if (exampledata === null) { + return ""; + } + + //console.log("NOT REPLACING ON PURPOSE!!") + //return "" + + // Basically just a stupid if-else :) + const synonyms = { + id: [ + "id", + "ref", + "sourceref", + "reference", + "sourcereference", + "alert id", + "case id", + "incident id", + "service id", + "sid", + "uid", + "uuid", + "team id", + "message id", + "message_id", + ], + title: ["title", "name", "message"], + description: ["description", "explanation", "story", "details"], + email: ["mail", "email", "sender", "receiver", "recipient"], + data: [ + "data", + "ip", + "domain", + "url", + "hash", + "md5", + "sha2", + "sha256", + "value", + "item", + ], + tags: ["tags", "taxonomies"], + }; + + // 1. Find the right synonym + // 2. Replace with an autocomplete if it exists + var selectedsynonyms = [paramname]; + for (const [key, value] of Object.entries(synonyms)) { + if (key === paramname || value.includes(paramname)) { + if (!value.includes(key)) { + value.push(key.toLowerCase()); + } + + selectedsynonyms = value; + break; + } + } + + var toreturn = ""; + + for (const [key, value] of Object.entries(exampledata)) { + // Check if loop or JSON + + if (typeof value === "object") { + if (Array.isArray(value)) { + var selectedkey = ""; + if (isNaN(key)) { + selectedkey = `.${key}`; + } + + for (let [subitem,subitemval] in Object.entries(value)) { + toreturn = GetParamMatch( + paramname, + value[subitem], + `${basekey}${selectedkey}.#` + ); + if (toreturn.length > 0) { + break; + } + } + + if (toreturn.length > 0) { + break; + } + + } else { + var selectedkey = ""; + if (isNaN(key)) { + selectedkey = `.${key}`; + } + + toreturn = GetParamMatch( + paramname, + value, + `${basekey}${selectedkey}` + ); + if (toreturn.length > 0) { + break; + } + } + } else { + if (selectedsynonyms.includes(key.toLowerCase())) { + toreturn = `${basekey}.${key}` + break + } + } + } + + return toreturn; + }; + + // Takes an action as input, then runs through and updates the relevant fields + // based on previous actions' + // Uses lots of synonyms + // autocomplete + const RunAutocompleter = (dstdata) => { + // **PS: The right action should already be set here** + // 1. Check execution argument + // 2. Check parents in order + var exampledata = GetExampleResult({ id: "exec", name: "exec" }); + var parentlabel = "exec"; + for (var dstdataParamKey in dstdata.parameters) { + const param = dstdata.parameters[dstdataParamKey]; + // Skip authentication params + if (param.configuration) { + continue + } + + if (param.options !== undefined && param.options !== null && param.options.length > 0) { + continue + } + + const paramname = param.name.toLowerCase().trim().replaceAll("_", " "); + + const foundresult = GetParamMatch(paramname, exampledata, ""); + if (foundresult.length > 0) { + console.log("FOUND ReS for field: ", dstdata.parameters[dstdataParamKey].name, foundresult) + if (dstdata.parameters[dstdataParamKey].value.length === 0) { + dstdata.parameters[dstdataParamKey].value = `$${parentlabel}${foundresult}`; + dstdata.parameters[dstdataParamKey].autocompleted = true + } + } + } + + var parents = getParents(dstdata); + if (parents.length > 1) { + for (let parentkey in parents) { + const item = parents[parentkey]; + if (item.label === "Execution Argument") { + continue; + } + + parentlabel = + item.label === undefined + ? "" + : item.label.toLowerCase().trim().replaceAll(" ", "_"); + + exampledata = GetExampleResult(item); + if (dstdata.parameters !== undefined && dstdata.parameters !== null) { + for (let [paramkey,paramkeyval] in Object.entries(dstdata.parameters)) { + const param = dstdata.parameters[paramkey]; + // Skip authentication params + if (param.configuration) { + continue + } + + if (param.options !== undefined && param.options !== null && param.options.length > 0) { + continue + } + + const paramname = param.name + .toLowerCase() + .trim() + .replaceAll("_", " "); + + const foundresult = GetParamMatch(paramname, exampledata, ""); + if (foundresult.length > 0) { + if (dstdata.parameters[paramkey].value.length === 0) { + dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + dstdata.parameters[paramkey].autocompleted = true + } else { + //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + } + } + } } + // Check agains every param + } + } - setSelectedApp(curapp) - setSelectedAction(curaction) + return dstdata; + }; - cy.removeListener('drag') - cy.removeListener('free') - cy.on('drag', 'node', (e) => onNodeDrag(e, curaction)) - cy.on('free', 'node', (e) => onNodeDragStop(e, curaction)) - } + // Checks for errors in edges when they're added + const onEdgeAdded = (event) => { + setLastSaved(false); + const edge = event.target.data(); - if (environments !== undefined && environments !== null) { - var env = environments.find(a => a.Name === curaction.environment) - if (!env || env === undefined) { - env = environments[defaultEnvironmentIndex] - } + console.log("edge added: ", edge) + if (edge.source === undefined && edge.target === undefined) { + return + } - setSelectedActionEnvironment(env) - } + if (edge.readded === true) { + console.log("Readded edge - stopping") - } else if (data.type === "TRIGGER") { - //console.log("Should handle trigger "+data.triggertype) - //console.log(data) - const trigger_index = workflow.triggers.findIndex(a => a.id === data.id) + event.target.data("readded", false) + return + } - if (data.app_name === "Shuffle Workflow") { - getAvailableWorkflows(trigger_index) - getSettings() - } else if (data.app_name === "Webhook") { - if (workflow.triggers[trigger_index].parameters !== undefined) { - workflow.triggers[trigger_index].parameters[0] = {"name": "url", "value": referenceUrl+"webhook_"+selectedTrigger.id} - } - } + const sourcenode = cy.getElementById(edge.source) + const destinationnode = cy.getElementById(edge.target) + if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { + } 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") { + if (sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { + setTimeout(() => { + const alledges = cy.edges().jsons() + console.log("edges: ", alledges, edge) + var targetedge = alledges.findIndex( + (data) => data.data.source === edge.source && data.data.id !== edge.id + ) - setSelectedTriggerIndex(trigger_index) - setSelectedTrigger(data) - setSelectedActionEnvironment(data.env) - } else { - alert.error("Can't handle "+data.type) - } + console.log("Node: ", targetedge) + if (targetedge !== -1) { + event.target.remove() - //console.log("BAR: ", rightSideBarOpen, "SAVE: ", lastSaved) - setRightSideBarOpen(true) - setLastSaved(false) + //console.log("Found branch already!") + alert.info("Triggers can have exactly one target node") + return - // Refresh listeners - //cy.removeListener('select') - //cy.on('select', 'node', (e) => onNodeSelect(e, newAppAuth, curapp, rightSideBarOpen, lastSaved)) - //cy.on('select', 'edge', (e) => onEdgeSelect(e)) - setScrollConfig({ - top: 0, - left: 0, - selected: "", - }) - } + // name: "Shuffle Workflow", + // name: "User Input", + } else { + console.log("Node doesn't already have one") + } + }, 50) + } + } - const GetExampleResult = (item) => { - var exampledata = item.example === undefined ? "" : item.example - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false - for (var key in workflowExecutions) { - if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { - continue - } + const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) + const currentedge = cy.getElementById(edge.id) + if (currentedge !== undefined && currentedge !== null) { + console.log("Setting edge curve: ", edgeCurve) + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) + } + } - var foundResult = {"result": ""} - if (item.id === "exec") { - //console.log("EXEC: ", workflowExecutions[key].execution_argument) - if (workflowExecutions[key].execution_argument !== undefined && workflowExecutions[key].execution_argument !== null && workflowExecutions[key].execution_argument.length > 0) { - foundResult.result = workflowExecutions[key].execution_argument - } else { - continue - } - } else { - foundResult = workflowExecutions[key].results.find(result => result.action.id === item.id) - if (foundResult === undefined) { - continue - } - } - foundResult.result = foundResult.result.trim() - foundResult.result = foundResult.result.split(" None").join(" \"None\"") - foundResult.result = foundResult.result.split(" False").join(" false") - foundResult.result = foundResult.result.split(" True").join(" true") + var targetnode = workflow.triggers.findIndex( + (data) => data.id === edge.target + ); + if (targetnode !== -1) { + console.log("TARGETNODE: ", targetnode); + if ( + workflow.triggers[targetnode].app_name === "User Input" || + workflow.triggers[targetnode].app_name === "Shuffle Workflow" + ) { + } else { + alert.error("Can't have triggers as target of branch"); + event.target.remove(); + } + } - var jsonvalid = true - try { - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false - } - } catch (e) { - try { - foundResult.result = foundResult.result.split("\'").join("\"") - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - } + const eventTarget = event.target.target() + //console.log("BUTTON ADDED! Find parent from: ", eventTarget) + if (eventTarget.data("isButton") === true) { + const parentNode = cy.getElementById(eventTarget.data("attachedTo")) + event.target.remove() + console.log("Setting it to parentnode: ", parentNode.data()) + if (parentNode !== undefined && parentNode !== null) { + //event.target.data("target", eventTarget.data("attachedTo")) - // Finds the FIRST json only - if (jsonvalid) { - //console.log("VALID!") - exampledata = JSON.parse(foundResult.result) - break - } else { - //console.log("INVALID: ", foundResult.result) - } - } - } + const newEdgeUuid = uuidv4() + const newcybranch = { + source: event.target.data("source"), + target: eventTarget.data("attachedTo"), + _id: newEdgeUuid, + id: newEdgeUuid, + hasErrors: event.target.data("hasErrors"), + }; - //console.log("EXAMPLE: ", exampledata) - return exampledata - } + const edgeToBeAdded = { + group: "edges", + data: newcybranch, + } - const GetParamMatch = (paramname, exampledata, basekey) => { - const splitkey = "." - //console.log(typeof(exampledata)) - //console.log("MATCHING WITH: ", exampledata) - if (typeof(exampledata) !== "object") { - return "" - } + cy.add(edgeToBeAdded); + } + } - if (exampledata === null) { - return "" - } + if ( + eventTarget.data("isDescriptor") === true || + eventTarget.data("type") === "COMMENT" + ) { + console.log("Removing because of descriptor or comment") + event.target.remove(); + return; + } - // Basically just a stupid if-else :) - const synonyms = { - "id": ["id", "ref", "sourceref", "reference", "sourcereference", "alert id", "case id", "incident id", "service id", "sid", "uid", "uuid", "team id"], - "title": ["title", "name", "message"], - "description": ["description", "explanation", "story", "details",], - "email": ["mail", "email", "sender", "receiver", "recipient"], - "data": ["data", "ip", "domain", "url", "hash", "md5", "sha2", "sha256", "value", "item",], - } + targetnode = -1; - // 1. Find the right synonym - // 2. - var selectedsynonyms = [paramname] - for (const [key, value] of Object.entries(synonyms)) { - if (key === paramname || value.includes(paramname)) { - if (!value.includes(key)) { - value.push(key.toLowerCase()) - } + // Check if: + // dest == source && source == dest + // dest == dest && source == source + // backend: check all children? to stop recursion + var found = false; + for (let branchkey in workflow.branches) { + if ( + workflow.branches[branchkey].destination_id === edge.source && + workflow.branches[branchkey].source_id === edge.target + ) { + alert.error("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 + ) { + console.log(edge.source); + alert.error("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") { + alert.error("Can't make arrow to starting node"); + event.target.remove(); + break; + } - selectedsynonyms = value - break - } - } - //console.log("SELECTED: ", selectedsynonyms) + found = true; + } + } 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 + // targetnode = workflow.triggers.findIndex(data => data.id === edge.source) + // console.log("Destination: ", edge.target) + // console.log("CHECK SOURCE IF ITS A TRIGGER: ", targetnode) + // if (targetnode !== -1) { + // alert.error("Triggers can only target one target (startnode)") + // event.target.remove() + // found = true + // break + // } + } else { + //console.log("INSIDE LAST CHECK: ", edge) + // Find the targetnode and check if its a trigger + // FIXME - do this for both actions and other types? + /* + targetnode = workflow.triggers.findIndex(data => data.id === edge.target) + if (targetnode !== -1) { + console.log("TARGETNODE: ", targetnode) + if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") { + } else { + alert.error("Can't have triggers as target of branch") + event.target.remove() + found = true + break + } + } + */ + } + } - //console.log("SYNONYMS FOR ", paramname, selectedsynonyms) - var toreturn = "" + // 1. Guess what the next node's action should be + // 2. Get result from previous nodes (if any) + // 3. TRY to automatically map them in based on synonyms + const newsource = cy.getElementById(edge.source); + const newdst = cy.getElementById(edge.target); + if ( + newsource !== undefined && + newsource !== null && + newdst !== undefined && + newdst !== null + ) { + const dstdata = RunAutocompleter(newdst.data()); + console.log("AUTO DST: ", dstdata); + } - for (const [key, value] of Object.entries(exampledata)) { - // Check if loop or JSON - const extra = basekey.length > 0 ? splitkey : "" - const basekeyname = `${basekey.slice(1, basekey.length).split(".").join(splitkey)}${extra}${key}` + var newbranch = { + source_id: edge.source, + destination_id: edge.target, + id_: edge.id, + id: edge.id, + hasErrors: false, + decorator: false, + }; - // Handle direct loop! - //if (!isNaN(key) && basekey === "") { - // //console.log("Handling direct loop: ", key, value) - // //parsedValues.push({"type": "object", "name": "Node", "autocomplete": `${basekey}`}) - // //parsedValues.push({"type": "list", "name": `${splitkey}list`, "autocomplete": `${basekey}.#`}) - // //for (var subkey in returnValues) { - // // parsedValues.push(returnValues[subkey]) - // //} + if (!found) { + newbranch["hasErrors"] = false; - // toreturn = GetParsedPaths(paramname, value, `${basekey}.#`) - // console.log("LIST, TORETURN: ", value, toreturn) - // if (toreturn.length > 0) { - // break - // } - //} + workflow.branches.push(newbranch); + setWorkflow(workflow); + } - //console.log("KEY: ", key, "VALUE: ", value, "BASEKEY: ", basekeyname) - if (typeof(value) === 'object') { - if (Array.isArray(value)) { - //console.log("LIST!!: ", value, key) - var selectedkey = "" - if (isNaN(key)) { - selectedkey = `.${key}` - } + history.push({ + type: "edge", + action: "added", + data: edge, + }); + setHistory(history); + setHistoryIndex(history.length); + }; - for (var subitem in value) { - toreturn = GetParamMatch(paramname, value[subitem], `${basekey}${selectedkey}.#`) - if (toreturn.length > 0) { - break - } - } + const onNodeAdded = (event) => { + const node = event.target; + const nodedata = event.target.data(); - if (toreturn.length > 0) { - break - } - } else { - var selectedkey = "" - if (isNaN(key)) { - selectedkey = `.${key}` - } + //if (Object.keys(nodedata).length === 1) { + // console.log("Check if another node actually exists before adding") + //} - toreturn = GetParamMatch(paramname, value, `${basekey}${selectedkey}`) - //console.log("OBJECT: ", value, toreturn, key) - if (toreturn.length > 0) { - break - } - } - //console.log("VALUE IS OBJECT: ", key, value) - } else { - //console.log("SINGLE ITEM: ", key) - if (selectedsynonyms.includes(key.toLowerCase())) { - //parsedValues.push({"type": "value", "name": basekeyname, "autocomplete": `${basekey}.${key}`, "value": value,}) - //console.log("STRING: ", key, value) - toreturn = `${basekey}.${key}` - //toreturn = basekeyname - break - } - } - } + if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined) + ) { + //if (nodedata.app_id === undefined) { + //console.log("Returning because node is not valid: ", nodedata) + return; + } - return toreturn - } - // Takes an action as input, then runs through and updates the relevant fields - // based on previous actions' - const RunAutocompleter = (dstdata) => { - // **PS: The right action should already be set here** - // 1. Check execution argument - // 2. Check parents in order - var exampledata = GetExampleResult({"id": "exec", "name": "exec",}) - //console.log("EXAMPLE RETURN: ", exampledata) - var parentlabel = "exec" - for (var paramkey in dstdata.parameters) { - const param = dstdata.parameters[paramkey] - // Skip authentication params - if (param.configuration) { + // DONT MOVE THIS LINE RIGHT HERE v + setLastSaved(false) + // Dont move the line above. May break stuff. + + + if (node.isNode() && cy.nodes().size() === 1) { + workflow.start = node.data("id"); + nodedata.isStartNode = true; + } else { + if (workflow.actions === null) { + console.log("Returning because node has no value") + return; + } + + // Remove bad startnode + for (let actionkey in workflow.actions) { + const action = workflow.actions[actionkey]; + if (action.isStartNode && workflow.start !== action.id) { + action.isStartNode = false; + } + } + } + + if (nodedata.type === "ACTION") { + /* + var curaction = workflow.actions.find((a) => a.id === nodedata.id); + if (curaction === null || curaction === undefined) { + alert.error("Node not found. Please remake it.") + event.target.remove(); + } + */ + if ( + workflow.actions.length === 1 && + workflow.actions[0].id === workflow.start + ) { + const newEdgeUuid = uuidv4(); + const newcybranch = { + source: workflow.start, + target: nodedata.id, + _id: newEdgeUuid, + id: newEdgeUuid, + hasErrors: false, + }; + + const edgeToBeAdded = { + group: "edges", + data: newcybranch, + }; + + console.log("SHOULD STITCH WITH STARTNODE"); + cy.add(edgeToBeAdded); + } + + if (nodedata.app_name === "Shuffle Tools") { + const iconInfo = GetIconInfo(nodedata); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + nodedata.large_image = svgpin_Url; + nodedata.fillGradient = iconInfo.fillGradient; + nodedata.fillstyle = "solid"; + if ( + nodedata.fillGradient !== undefined && + nodedata.fillGradient !== null && + nodedata.fillGradient.length > 0 + ) { + nodedata.fillstyle = "linear-gradient"; + } else { + nodedata.iconBackground = iconInfo.iconBackgroundColor; + } + } + + + if ( + nodedata.parameters !== undefined && + nodedata.parameters !== null && + !nodedata.label.endsWith("_copy") + ) { + var newparameters = []; + + for (let [subkey,subkeyval] in Object.entries(nodedata.parameters)) { + var newparam = JSON.parse( + JSON.stringify(nodedata.parameters[subkey]) + ); + newparam.id = uuidv4(); + + if (newparam.value === undefined || newparam.value === null) { + newparam.value = ""; + } else { + newparam.value = newparam.value; + } + + newparameters.push(newparam); + } + + nodedata.parameters = newparameters; + } + + if (workflow.actions === undefined || workflow.actions === null) { + workflow.actions = [nodedata]; + } else { + workflow.actions.push(nodedata); + } + + setWorkflow(workflow); + } else if (nodedata.type === "TRIGGER") { + if (nodedata.is_valid === false) { + alert.info("This trigger is not available to you"); + node.remove(); + return; + } + + if (workflow.triggers === undefined) { + workflow.triggers = [nodedata]; + } else { + workflow.triggers.push(nodedata); + } + + const newEdgeUuid = uuidv4(); + const newcybranch = { + source: nodedata.id, + target: workflow.start, + source_id: nodedata.id, + destination_id: workflow.start, + _id: newEdgeUuid, + id: newEdgeUuid, + hasErrors: false, + decorator: false, + }; + + const edgeToBeAdded = { + group: "edges", + data: newcybranch, + }; + + if ( + nodedata.name !== "User Input" && + nodedata.name !== "Shuffle Workflow" + ) { + if ( + workflow.actions !== undefined && + workflow.actions !== null && + workflow.actions.length > 0 + ) { + cy.add(edgeToBeAdded); + } + } + + setWorkflow(workflow); + } + + if (nodedata.app_name !== undefined) { + history.push({ + type: "node", + action: "added", + data: nodedata, + }); + setHistory(history); + setHistoryIndex(history.length); + } + }; + + const onEdgeRemoved = (event) => { + setLastSaved(false); + + const edge = event.target; + if (edge.data("decorator") === true) { + return; + } + + // Check if the source is trigger and can start + console.log("Removed: ", edge.data()) + const allNodes = cy.nodes().jsons() + for (let nodekey in allNodes) { + const curnode = allNodes[nodekey] + if (curnode.data.type !== "TRIGGER") { continue } - const paramname = param.name.toLowerCase().trim().replaceAll("_", " ") - //console.log("PARAM: ", param) - //console.log("PARAMNAME: ", paramname) + if (curnode.data.id === edge.data("source")) { + console.log("Found matching trigger source: ", curnode) + if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { + // If it's started, READD the edge + if (curnode.data.status === "running") { + console.log("Edge is running - readd it: ", edge.data()) - const foundresult = GetParamMatch(paramname, exampledata, "") - if (foundresult.length > 0) { - //console.log("FOUND: ", paramname, foundresult) + // Just making sure it's not running infinitely + var newdata = edge.data() + newdata.readded = true - if (dstdata.parameters[paramkey].value.length === 0) { - dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}` - } else { - //console.log("Skipping ", dstdata.parameters[paramkey], " because it already has a value") - } + try { + cy.add({ + group: "edges", + data: newdata, + }) + + alert.error("You must STOP the trigger before deleting its branches") + } catch (e) { + console.log("Failed re-adding edge: ", e) + } + } + + //status: "uninitialized", + } + } + } + + workflow.branches = workflow.branches.filter( + (a) => a.id !== edge.data().id + ); + + setWorkflow(workflow); + event.target.remove(); + + // trigger as source check + const indexcheck = workflow.triggers.findIndex( + (data) => edge.data("source") === data.id + ); + if (indexcheck !== -1) { + console.log("Shouldnt remove edge from trigger? "); + } + + if (edge.data().source !== undefined) { + history.push({ + type: "edge", + action: "removed", + data: edge.data(), + }); + + setHistory(history); + setHistoryIndex(history.length); + } + }; + + const onNodeRemoved = (event) => { + const node = event.target; + const data = node.data(); + + if (data.finished === false) { + return + } + + workflow.actions = workflow.actions.filter((a) => a.id !== data.id); + workflow.triggers = workflow.triggers.filter((a) => a.id !== data.id); + if (workflow.start === data.id && workflow.actions.length > 0) { + // FIXME - should check branches connected to startnode, as picking random + // is just confusing + if (workflow.actions[0].id !== data.id) { + const ele = cy.getElementById(workflow.actions[0].id); + if (ele !== undefined && ele !== null) { + ele.data("isStartNode", true); + workflow.start = ele.id(); + } + } else { + if (workflow.actions.length > 1) { + const ele = cy.getElementById(workflow.actions[1].id); + if (ele !== undefined && ele !== null) { + ele.data("isStartNode", true); + workflow.start = ele.id(); + } + } + } + } + + if (data.app_name !== undefined) { + const allNodes = cy.nodes().jsons(); + for (let allNodesKey in allNodes) { + const currentNode = allNodes[allNodesKey]; + if (currentNode.data.attachedTo === data.id) { + cy.getElementById(currentNode.data.id).remove(); + } + } + + history.push({ + type: "node", + action: "removed", + data: data, + }) + + //console.log("REMOVED: ", data) + setHistory(history); + setHistoryIndex(history.length); + } + + setWorkflow(workflow); + if (data.type === "TRIGGER") { + saveWorkflow(workflow); + } + }; + + //var previouskey = 0 + const handleKeyDown = (event) => { + switch (event.keyCode) { + case 27: + if (configureWorkflowModalOpen === true) { + setConfigureWorkflowModalOpen(false); + } + break; + case 46: + console.log("DELETE"); + break; + case 38: + //console.log("UP"); + break; + case 37: + //console.log("LEFT"); + break; + case 40: + //console.log("DOWN"); + break; + case 39: + //console.log("RIGHT"); + break; + case 90: + if (event.ctrlKey) { + console.log("CTRL+Z"); + } + + break; + case 67: + if (event.ctrlKey && !event.shiftKey) { + if ( + event.path !== undefined && + event.path !== null && + event.path.length > 0 + ) { + if (event.path[0].localName !== "body") { + console.log("Skipping because body is not targeted"); + return; + } + } + + if ( + event.target !== undefined && + event.target !== null + ) { + if (event.target.localName !== "body") { + console.log("Skipping because body is not targeted") + return; + } + } + + console.log("CTRL+C"); + if (cy !== undefined) { + var cydata = cy.$(":selected").jsons(); + if (cydata !== undefined && cydata !== null && cydata.length > 0) { + console.log(cydata); + + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(JSON.stringify(cydata)); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + document.execCommand("copy"); + alert.success(`Copied ${cydata.length} element(s)`); + } + } + } + } + break; + case 86: + if (event.ctrlKey) { + //console.log("CTRL+V") + // The below parts are handled in the function handlePaste() + /* + const clipboard = navigator.clipboard + if (clipboard === undefined || window === undefined || window === null) { + alert.error("Can only use cliboard over HTTPS (port 3443)") + return + } + + console.log("CLIPBOARD: ", window.clipboardData) + const pastedData = window.clipboardData.getData('Text'); + console.log("PASTED: ", pastedData) + + + //var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) + var jsonvalid = true + var parsedjson = [] + */ + } + break; + case 88: + if (event.ctrlKey) { + console.log("CTRL+X"); + } + break; + case 83: + break; + case 70: + break; + case 65: + // As a poweruser myself, I found myself hitting this a few + // too many times to just edit text. Need a better bind, which does NOT work while inside a field + break; + default: + break; + } + + //previouskey = event.keyCode + }; + + const handlePaste = (event) => { + //console.log("EV: ", event) + if ( + event.path !== undefined && + event.path !== null && + event.path.length > 0 + ) { + //console.log("PATH: ", event.path[0]) + if (event.path[0].localName !== "body") { + //console.log("Skipping because body is not targeted") + return; + } + } + + //console.log("PATH2: ", event.target) + if ( + event.target !== undefined && + event.target !== null + ) { + if (event.target.localName !== "body") { + //console.log("Skipping because body is not targeted") + return; + } + } + + + event.preventDefault(); + const clipboard = (event.originalEvent || event).clipboardData.getData( + "text/plain" + ); + //console.log("Text: ", clipboard) + //window.document.execCommand('insertText', false, text); + // + try { + var parsedjson = JSON.parse(clipboard); + // Check if array + if (!Array.isArray(parsedjson)) { + console.log("Not array! Adding to array.") + parsedjson = [parsedjson] } - } - - var parents = getParents(dstdata) - //console.log("PARENTS: ", parents) - if (parents.length > 1) { - for (var key in parents) { - const item = parents[key] - if (item.label === "Execution Argument") { - continue - } - parentlabel = item.label === undefined ? "" : item.label.toLowerCase().trim().replaceAll(" ", "_") - exampledata = GetExampleResult(item) - for (var paramkey in dstdata.parameters) { - const param = dstdata.parameters[paramkey] - // Skip authentication params - if (param.configuration) { - continue + for (let jsonkey in parsedjson) { + var item = parsedjson[jsonkey]; + console.log("Adding: ", item); + + if (item.data === undefined || item.data === null) { + console.log("Appending from here") + const newitem = { + "data": item, + "position": { + "x": 0, + "y": 0 + }, + "group": "nodes", } - const paramname = param.name.toLowerCase().trim().replaceAll("_", " ") - //console.log("PARAM: ", param) - //console.log("PARAMNAME: ", paramname) - - const foundresult = GetParamMatch(paramname, exampledata, "") - if (foundresult.length > 0) { - //console.log("FOUND: ", paramname, foundresult) - - if (dstdata.parameters[paramkey].value.length === 0) { - dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}` - } else { - dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}` - //console.log("Skipping ", dstdata.parameters[paramkey], " because it already has a value") - } - } - } - // Check agains every param - } - } - - return dstdata - } - - //const FixNameUpdater = (sourcenode) => { - //} - - // Checks for errors in edges when they're added - const onEdgeAdded = (event) => { - const edge = event.target.data() - console.log("EDGE ADDED: ", edge) - //setLastSaved(false) - var targetnode = workflow.triggers.findIndex(data => data.id === edge.target) - if (targetnode !== -1) { - console.log("TARGETNODE: ", targetnode) - if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") { - } else { - alert.error("Can't have triggers as target of branch") - event.target.remove() - } - } - - console.log("TARGET: ", event.target.target().data()) - if (event.target.target().data("isButton") === true || event.target.target().data("isDescriptor") === true) { - event.target.remove() - return - } - - targetnode = -1 - var sourcenode = workflow.triggers.findIndex(data => data.id === edge.source) - console.log("SOURCENODE: ", sourcenode) - if (sourcenode !== -1) { - if (workflow.triggers[sourcenode].app_name === "User Input" || workflow.triggers[sourcenode].app_name === "Shuffle Workflow") { - //console.log("NORMAL TRIGGER") - } else { - //var currentnode = cy.getElementById(workflow.triggers[sourcenode].id) - //console.log("NODE: ", currentnode) - //if (currentnode !== null && currentnode !== undefined) { - // console.log("SHOULD CHECK IF TRIGGER HAS MULTIPLE EDGES: ", currentnode) - //if (workflow.branches !== undefined && workflow.branches !== null) { - // const found_branches = workflow.branches.filter(branch => branch.source == workflow.triggers[sourcenode].id) - // console.log("FOUND BRANCHES: ", found_branches) - // if (found_branches.length > 0) { - // alert.error("Can't have multiple branches from this trigger") - // event.target.remove() - // } - //} - - //if (cy.edges().size() === 1) { - // https://js.cytoscape.org/#edges.connectedNodes - //console.log("CURRENTNODE: ", currentnode) - //console.log("EDGES: ", currentnode.connectedEdges(`node[id=${workflow.triggers[sourcenode].id}]`)) - //console.log("EDGES2: ", currentnode.connectedEdges()) - //currentnode.connectedEdges().animate({style: {lineColor: "red"}}) - //console.log("OUTGOERS: ", currentnode.outgoers()) - - //console.log("LEN2: ", currentnode.edges().length) - //if (currentnode.connectedNodes().length > 0) { - // alert.error("Can't have multiple branches from this trigger") - // event.target.remove() - //} - } - } - - - //console.log(workflow.branches) - - // Check if: - // dest == source && source == dest - // dest == dest && source == source - // backend: check all children? to stop recursion - var found = false - for (var key in workflow.branches) { - if (workflow.branches[key].destination_id === edge.source && workflow.branches[key].source_id === edge.target) { - alert.error("A branch in the opposite direction already exists") - event.target.remove() - found = true - break - } else if (workflow.branches[key].destination_id === edge.target && workflow.branches[key].source_id === edge.source) { - console.log(edge.source) - alert.error("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) { - alert.error("Can't make arrow to starting node") - event.target.remove() - found = true - break - } - } else if (edge.source === workflow.branches[key].source_id) { - // FIXME: Verify multi-target for triggers - // 1. Check if destination exists - // 2. Check if source is a trigger - // targetnode = workflow.triggers.findIndex(data => data.id === edge.source) - // console.log("Destination: ", edge.target) - // console.log("CHECK SOURCE IF ITS A TRIGGER: ", targetnode) - // if (targetnode !== -1) { - // alert.error("Triggers can only target one target (startnode)") - // event.target.remove() - // found = true - // break - // } - } else { - //console.log("INSIDE LAST CHECK: ", edge) - - // Find the targetnode and check if its a trigger - // FIXME - do this for both actions and other types? - /* - targetnode = workflow.triggers.findIndex(data => data.id === edge.target) - if (targetnode !== -1) { - console.log("TARGETNODE: ", targetnode) - if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") { - } else { - alert.error("Can't have triggers as target of branch") - event.target.remove() - found = true - break - } - } - */ - - - } - } - - - // 1. Guess what the next node's action should be - // 2. Get result from previous nodes (if any) - // 3. TRY to automatically map them in based on synonyms - const newsource = cy.getElementById(edge.source) - const newdst = cy.getElementById(edge.target) - if (newsource !== undefined && newsource !== null && newdst !== undefined && newdst !== null) { - //const srcdata = newsource.data() - //console.log("EDGE: ", edge) - const dstdata = RunAutocompleter(newdst.data()) - console.log("DST: ", dstdata) - } - - var newbranch = { - "source_id": edge.source, - "destination_id": edge.target, - "id_": edge.id, - "id": edge.id, - "hasErrors": false, - "decorator": false, - } - - if (!found) { - newbranch["hasErrors"] = false - - workflow.branches.push(newbranch) - setWorkflow(workflow) - } - - history.push({ - "type": "edge", - "action": "added", - "data": edge, - }) - setHistory(history) - setHistoryIndex(history.length) - } - - const onNodeAdded = (event) => { - const node = event.target - const nodedata = event.target.data() - if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined)) { - //console.log("RETURNING (NOT ADDING) NODE ADD FOR: ", nodedata) - return - } - - //console.log("IS IT ADDED TO THE WORKFLOW?: ", nodedata) - if (node.isNode() && cy.nodes().size() === 1) { - //setStartNode(node.data('id')) - workflow.start = node.data('id') - nodedata.isStartNode = true - } else { - if (workflow.actions === null) { - return - } - - // Remove bad startnode - const startnode_exists = false - for (var key in workflow.actions) { - const action = workflow.actions[key] - if (action.isStartNode && workflow.start !== action.id) { - action.isStartNode = false - } - } - } - - if (nodedata.type === "ACTION") { - /* - const tmpView = localStorage.getItem(appSorter) - const nodekey = nodedata.id - if (tmpView === null || tmpView === undefined) { - const basedata = {} - basedata[nodekey] = 1 - localStorage.setItem(appSorter, JSON.stringify(basedata)) - } else { - try { - var parsed = JSON.parse(tmpView) - try { - parsed[nodekey] += 1 - } catch { - parsed[nodekey] = 1 - } - - localStorage.setItem(appSorter, JSON.stringify(parsed)) - } catch { - console.log("Bad data in tmpView: ", tmpView) - } - } - console.log("FAVORITeS: ", tmpView) - */ - - if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) { - const newEdgeUuid = uuidv4() - const newcybranch = { - "source": workflow.start, - "target": nodedata.id, - "_id": newEdgeUuid, - "id": newEdgeUuid, - "hasErrors": false, + item = newitem + item.type = "ACTION" + item.isStartNode = false + item.data.type = "ACTION" + item.data.isStartNode = false } - const edgeToBeAdded = { - group: "edges", - data: newcybranch, - } + item.data.id = uuidv4() - console.log("SHOULD STITCH WITH STARTNODE") - cy.add(edgeToBeAdded) - } - - if (nodedata.app_name === "Shuffle Tools") { - const iconInfo = GetIconInfo(nodedata) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - nodedata.large_image = svgpin_Url - nodedata.fillGradient = iconInfo.fillGradient - nodedata.fillstyle = "solid" - if (nodedata.fillGradient !== undefined && nodedata.fillGradient !== null && nodedata.fillGradient.length > 0) { - nodedata.fillstyle = 'linear-gradient' - } else { - nodedata.iconBackground = iconInfo.iconBackgroundColor - } - } + cy.add({ + group: item.group, + data: item.data, + position: item.position, + }); + } + } catch (e) { + console.log("Error pasting: ", e); + //alert.info("Failed parsing clipboard: ", e) + } + }; - if (nodedata.parameters !== undefined && nodedata.parameters !== null && !nodedata.label.endsWith("_copy")) { - var newparameters = [] - - for (var subkey in nodedata.parameters) { - var newparam = JSON.parse(JSON.stringify(nodedata.parameters[subkey])) - newparam.id = uuidv4() - newparam.value = "" - newparameters.push(newparam) - } + const registerKeys = () => { + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("paste", handlePaste); + }; - nodedata.parameters = newparameters - } - - if (workflow.actions === undefined || workflow.actions === null) { - workflow.actions = [nodedata] - } else { - workflow.actions.push(nodedata) - } - - setWorkflow(workflow) - } else if (nodedata.type === "TRIGGER") { - if (nodedata.is_valid === false) { - alert.info("This trigger is not available to you") - node.remove() - return - } - - if (workflow.triggers === undefined) { - workflow.triggers = [nodedata] - } else { - workflow.triggers.push(nodedata) - } - - const newEdgeUuid = uuidv4() - const newcybranch = { - "source": nodedata.id, - "target": workflow.start, - "source_id": nodedata.id, - "destination_id": workflow.start, - "_id": newEdgeUuid, - "id": newEdgeUuid, - "hasErrors": false, - "decorator": false, - } - - const edgeToBeAdded = { - group: "edges", - data: newcybranch, - } - - if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { - //workflow.branches.push(newbranch) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - cy.add(edgeToBeAdded) - } - } - - //if (data.trigger_type === "WEBHOOK") { - // newWebhook(newAppData) - // saveWorkflow(workflow) - //} - - setWorkflow(workflow) - } - - - if (nodedata.app_name !== undefined) { - history.push({ - "type": "node", - "action": "added", - "data": nodedata, - }) - setHistory(history) - setHistoryIndex(history.length) - } - - //if (nodedata.app_name !== undefined && (( - // nodedata.app_name !== "Shuffle Tools" && - // nodedata.app_name !== "Testing" && - // nodedata.app_name !== "Shuffle Workflow" && - // nodedata.app_name !== "User Input" && - // nodedata.app_name !== "Webhook" && - // nodedata.app_name !== "Schedule" && - // nodedata.app_name !== "Email") || nodedata.isStartNode) - //) { - // const allNodes = cy.nodes().jsons() - // var found = false - // for (var key in allNodes) { - // const currentNode = allNodes[key] - // if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) { - // found = true - // console.log("FOUND THE NODE!") - // break - // } - // } - - // // Readding the icon after moving the node - // if (!found) { - // console.log("Node wasn't found") - // const iconInfo = GetIconInfo(nodedata) - // const svg_pin = `` - // const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - // const offset = nodedata.isStartNode ? 36 : 44 - // const decoratorNode = { - // position: { - // x: event.target.position().x+offset, - // y: event.target.position().y+offset, - // }, - // locked: true, - // data: { - // "isDescriptor": true, - // "isValid": true, - // "is_valid": true, - // "label": "", - // "image": svgpin_Url, - // "imageColor": iconInfo.iconBackgroundColor, - // "attachedTo": nodedata.id, - // }, - // } - - // cy.add(decoratorNode) - // } else { - // console.log("Node already exists - don't add descriptor node") - // } - //} else { - // //console.log("Shouldnt re-add info? ") - //} - } - - const onEdgeRemoved = (event) => { - setLastSaved(false) - const edge = event.target - - workflow.branches = workflow.branches.filter(a => a.id !== edge.data().id) - setWorkflow(workflow) - event.target.remove() - - // trigger as source check - const indexcheck = workflow.triggers.findIndex(data => edge.data()["source"] === data.id) - if (indexcheck !== -1) { - //alert.error("Can't remove edge from a trigger") - console.log("Shouldnt remove edge from trigger") - //const edgeToBeAdded = { - // group: "edges", - // data: newcybranch, - //} - } - - if (edge.data().source !== undefined) { - history.push({ - "type": "edge", - "action": "removed", - "data": edge.data().source, - }) - setHistory(history) - setHistoryIndex(history.length) - } - } - - const onNodeRemoved = (event) => { - const node = event.target - const data = node.data() - - if (data.finished === false) { - return - } - - //setLastSaved(false) - - workflow.actions = workflow.actions.filter(a => a.id !== data.id) - workflow.triggers = workflow.triggers.filter(a => a.id !== data.id) - if (workflow.start === data.id && workflow.actions.length > 0) { - // FIXME - should check branches connected to startnode, as picking random - // is just confusing - if (workflow.actions[0].id !== data.id) { - const ele = cy.getElementById(workflow.actions[0].id) - if (ele !== undefined && ele !== null) { - ele.data("isStartNode", true) - workflow.start = ele.id() - } - } else { - if (workflow.actions.length > 1) { - const ele = cy.getElementById(workflow.actions[1].id) - if (ele !== undefined && ele !== null) { - ele.data("isStartNode", true) - workflow.start = ele.id() - } - } - } - - //cy.nodes().some(function( ele ) { - // if (ele.id() !== workflow.start && ele.data()["label"] !== undefined) { - // //alert.success("Changed startnode to "+ele.data()["label"]) - // ele.data("isStartNode", true) - // workflow.start = ele.id() - // throw BreakException - // return false - // } - //}) - } - - if (data.app_name !== undefined) { - //console.log("Trying to remove friendly nodes") - const allNodes = cy.nodes().jsons() - //console.log("NOT UNDEFINED IN HOVEROUT!", allNodes) - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.attachedTo === data.id) { - cy.getElementById(currentNode.data.id).remove() - } - } - - history.push({ - "type": "node", - "action": "removed", - "data": data, - }) - setHistory(history) - setHistoryIndex(history.length) - } - - - setWorkflow(workflow) - if (data.type === "TRIGGER") { - saveWorkflow(workflow) - } - } - - var previouskey = 0 - const handleKeyDown = (event) => { - // SHIFT = 16 - // CTRL = 17 - //console.log(event.keyCode) - switch( event.keyCode ) { - case 27: - //console.log("ESCAPE") - if (configureWorkflowModalOpen === true) { - setConfigureWorkflowModalOpen(false) - } - break; - case 46: - //removeNode() - console.log("DELETE") - break; - case 38: - console.log("UP") - break; - case 37: - console.log("LEFT") - break; - case 40: - console.log("DOWN") - break; - case 39: - console.log("RIGHT") - break; - case 90: - if (previouskey === 17) { - console.log("CTRL+Z") - //handleHistoryUndo() - } - - break; - case 67: - if (previouskey === 17) { - console.log("CTRL+C") - //const filteredelements = cy.filter(function(element, i){ - // return element.hasClass('selected') - //}) - - //for (var key in filteredelements) { - //} - - //var copyText = document.getElementById("copy_element_shuffle") - //if (copyText !== undefined && copyText !== null) { - // const clipboard = navigator.clipboard - // if (clipboard === undefined) { - // alert.error("Can only copy over HTTPS (port 3443)") - // return - // } - //} - //console.log("FILTERED: ", filteredelements) - } - break; - case 86: - if (previouskey === 17) { - console.log("CTRL+V") - } - break; - case 88: - if (previouskey === 17) { - console.log("CTRL+V") - } - break; - case 83: - //if (previouskey === 17) { - // event.preventDefault() - // saveWorkflow() - //} - break; - case 70: - //if (previouskey === 17) { - // event.preventDefault() - // cy.fit(null, 50) - //} - //break; - case 65: - // As a poweruser myself, I found myself hitting this a few - // too many times to just edit text. Need a better bind - // - //if (previouskey === 17) { - // event.preventDefault() - // if (executionRunning || executionRequestStarted) { - // abortExecution() - // } else { - // executeWorkflow() - // } - // cy.fit(null, 50) - //} - break; - default: - //console.log(event.keyCode) - break; - } - - previouskey = event.keyCode - } - - const registerKeys = () => { - document.addEventListener("keydown", handleKeyDown); - } - - const getEnvironments = () => { - fetch(globalUrl+"/api/v1/getenvironments", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!") - if (isCloud) { - setEnvironments({"name": "Cloud", "type": "cloud"}) - } else { - setEnvironments({"name": "Onprem", "type": "onprem"}) - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for envs :O!"); + if (isCloud) { + setEnvironments([{ Name: "Cloud", Type: "cloud" }]); + } else { + setEnvironments([{ Name: "Onprem", Type: "onprem" }]); + } - return - } + return; + } - return response.json() - }) - .then((responseJson) => { - var found = false - var showEnvCnt = 0 - for (var key in responseJson) { - if (responseJson[key].default) { - setDefaultEnvironmentIndex(key) - found = true - } + return response.json(); + }) + .then((responseJson) => { + var found = false; + var showEnvCnt = 0; + for (let jsonkey in responseJson) { + if (responseJson[jsonkey].default && !found) { + setDefaultEnvironmentIndex(jsonkey); + found = true; + } - if (responseJson[key].archived === false) { - showEnvCnt += 1 - } - } + if (responseJson[jsonkey].archived === false) { + showEnvCnt += 1; + } + } - if (showEnvCnt > 1) { - setShowEnvironment(true) - } + if (showEnvCnt > 1) { + setShowEnvironment(true); + } - if (!found) { - for (var key in responseJson) { - if (!responseJson[key].archived) { - setDefaultEnvironmentIndex(key) - break - } - } - } + if (!found) { + for (let jsonkey in responseJson) { + if (!responseJson[jsonkey].archived) { + setDefaultEnvironmentIndex(jsonkey); + break; + } + } + } - setEnvironments(responseJson) - }) - .catch(error => { - alert.error(error.toString()) - }); + // FIXME: Don't allow multiple in cloud yet. Cloud -> Onprem isn't stable. + if (isCloud) { + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + setEnvironments(responseJson); + } else { + setEnvironments([{ Name: "Cloud", Type: "cloud" }]); + } + } else { + setEnvironments(responseJson); + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get environments error: ", error.toString()); + }); + }; + + if ( + !firstrequest && + graphSetup && + established && + props.match.params.key !== workflow.id && + workflow.id !== undefined && + workflow.id !== null && + workflow.id.length > 0 + ) { + window.location.pathname = "/workflows/" + props.match.params.key; + } + + const animationDuration = 150; + const onNodeHoverOut = (event) => { + // console.log("Hover out: ", event.target.data()); + + const nodedata = event.target.data(); + // console.log("nodedata", nodedata); + // console.log("nodedata.app_name: ", nodedata.app_name); + if (nodedata.app_name !== undefined) { + + const allNodes = cy.nodes().jsons(); + // console.log("allNodes: ", allNodes) + for (var nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + // console.log("Current node: ", currentNode); + if ( + currentNode.data.isButton && + currentNode.data.attachedTo !== nodedata.id + ) { + cy.getElementById(currentNode.data.id).remove(); + } + + } + } + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "default" + } + + // Skipping node editing if it's the selected one + if (cy !== undefined) { + const typeIds = cy.elements('node:selected').jsons(); + for (var idkey in typeIds) { + const item = typeIds[idkey] + if (item.data.id === nodedata.id) { + return + } + } + } + //if (nodedata.id === selectedAction.id || nodedata.id === selectedTrigger.id) { + // return + //} + + var parsedStyle = { + "border-width": "1px", + "font-size": "18px", + //"cursor": "default", + } + + if ((nodedata.app_name === "Testing" || nodedata.app_name === "Shuffle Tools") && !nodedata.isStartNode) { + parsedStyle = { + "border-width": "1px", + "font-size": "0px", + }; + } + + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ); + + const outgoingEdges = event.target.outgoers("edge"); + const incomingEdges = event.target.incomers("edge"); + if (outgoingEdges.length > 0) { + outgoingEdges.removeClass("hover-highlight"); + } + + if (incomingEdges.length > 0) { + outgoingEdges.removeClass("hover-highlight"); + } + }; + + const buttonColor = "rgba(255,255,255,0.9)"; + const buttonBackgroundColor = "#1f2023"; + const addStartnodeButton = (event) => { + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + + if (parentNode.data("isStartNode")) { + return; + } + + const px = parentNode.position("x") - 65; + const py = parentNode.position("y") - 45; + const circleId = (newNodeId = uuidv4()); + + parentNode.data("circleId", circleId); + + const iconInfo = { + icon: "M9.4 2H15V12H8L7.6 10H2V17H0V0H9L9.4 2ZM9 10H11V8H13V6H11V4H9V6L8 4V2H6V4H4V2H2V4H4V6H2V8H4V6H6V8H8V6L9 8V10ZM6 6V4H8V6H6ZM9 6H11V8H9V6Z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + cy.add({ + group: "nodes", + data: { + weight: 30, + id: circleId, + name: "TEEEXT", + isButton: true, + buttonType: "set_startnode", + attachedTo: event.target.data("id"), + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + is_valid: true, + }, + position: { x: px, y: py }, + locked: true, + }); + }; + + const addRunCountButton = (event) => { + // Count executions? + // Maybe it shouldn't be onclick? } + const addCopyButton = (event) => { + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + + const px = parentNode.position("x") - 65; + const py = parentNode.position("y") - 5; + const circleId = (newNodeId = uuidv4()); + + parentNode.data("circleId", circleId); + + const iconInfo = { + icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + cy.add({ + group: "nodes", + data: { + weight: 30, + id: circleId, + name: "TEEEXT", + isButton: true, + buttonType: "copy", + attachedTo: event.target.data("id"), + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + is_valid: true, + }, + position: { x: px, y: py }, + locked: true, + }); + }; + + const addSuggestionButtons = (nodedata, event) => { + console.log("In suggestion buttons: ", nodedata, ". This is not enabled yet. Backend needs further work.") + return + // Skipping add for now. Should Re-enable + + // Add a button for autocompletion based on input + console.log("Type: ", nodedata.type) + if (nodedata.type === "ACTION") { + const color = "#34a853" + + // Fix icon + const iconInfo = { + icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - if (!firstrequest && graphSetup && established && props.match.params.key !== workflow.id && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0) { - //console.log(props.match.params.key, workflow.id) - //getWorkflow() - //setCy() - //getWorkflowExecution(props.match.params.key, "") - //setEstablished(false) - //setGraphSetup(false) - window.location.pathname = "/workflows/"+props.match.params.key - } - - useEffect(() => { - if (firstrequest) { - setFirstrequest(false) - getWorkflow(props.match.params.key, {}) - getApps() - getAppAuthentication() - getEnvironments() - getWorkflowExecution(props.match.params.key, "") - getAvailableWorkflows(-1) - getSettings() - - const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search - const tmpView = new URLSearchParams(cursearch).get("view") - if (tmpView !== undefined && tmpView !== null && tmpView === "executions") { - setExecutionModalOpen(true) - - const newitem = removeParam("view", cursearch) - props.history.push(curpath+newitem) - } - return - } - - - // App length necessary cus of cy initialization - //console.log("PRE ELEMENTS: !", workflow.actions, graphSetup, apps, authLoaded, cy) - if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { - setGraphSetup(true) - setupGraph() - - //console.log("IN ELEMENT CHECK!") - } else if (!established && cy !== undefined && apps !== null && apps !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0 && authLoaded){ - //This part has to load LAST, as it's kind of not async. - //This means we need everything else to happen first. - // - //console.log("IN THIS PART AGAIN") - - //console.log("IN ESTABLISHED!") - - setEstablished(true) - // Validate if the node is just a node lol - //console.log("CY: ", cy) - //console.log("CY: ", cy.edgehandles()) - //try { - cy.edgehandles({ - handleNodes: (el) => el.isNode() && !el.data("isButton") && !el.data("isDescriptor"), - preview: false, - toggleOffOnLeave: false, - loopAllowed: function( node ){ - return false; - }, - }) - //} catch (e) { - // console.log("Error in edgehandles: ", e) - //} - - cy.fit(null, 200) - - cy.on('boxselect', 'node', (e) => { - if (e.target.data("isButton") || e.target.data("isDescriptor")) { - e.target.unselect() - } - - e.target.addClass("selected") - }) - - cy.on('boxstart', (e) => { - console.log("START") - cy.removeListener('select') - //onNodeSelect(e, appAuthentication) - }) - cy.on('boxend', (e) => { - console.log("END") - cy.removeListener('select') - //onNodeSelect(e, appAuthentication) - }) - - 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)) - - //let popper2 = cy.popper({ - // content: () => { - // let div = document.createElement('div'); - // - // div.innerHTML = 'Popper content' - // - // document.body.appendChild(div) - // - // return div - // }, - // renderedPosition: () => ({ x: 100, y: 200 }), - // popper: {} // my popper options here - //}); - - //let popper1 = cy.nodes()[0].popper({ - // content: () => { - // let div = document.createElement('div') - // - // div.innerHTML = 'Popper content' - // document.body.appendChild(div) - // - // return div - // }, - // popper: {} // my popper options here - //}) - //let update = () => { - // popper.update() - //} - - //let node = cy.nodes().first() - //node.on('position', update) - - - - - - //cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover')); - - //cy.on('cxttapstart', 'node', (e) => edgeHandler.start(e.target)) - //cy.on('cxttapend', 'node', (e) => edgeHandler.stop()) - //cy.on('cxtdragover', 'node', (e) => edgeHandler.preview(e.target)) - //cy.on('cxtdragout', 'node', (e) => edgeHandler.unpreview(e.target)) - - document.title = "Workflow - "+workflow.name - registerKeys() - //setStartNode(workflow.start) - } else if (established) { - //console.log("established - should fix colors of things") - //console.log(cy.elements()) - } - }) - - //}, [selectedAction]) - - var previousnodecolor = "" - //var previousedgecolor = "" - const animationDuration = 150 - const onNodeHoverOut = (event) => { - const nodedata = event.target.data() - if (nodedata.app_name !== undefined) { - const allNodes = cy.nodes().jsons() - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { - cy.getElementById(currentNode.data.id).remove() - } - } - } - - var parsedStyle = { - "border-width": "1px", - 'font-size': '18px', - } - - if ((nodedata.app_name === "Testing" || nodedata.app_name === "Shuffle Tools") && !nodedata.isStartNode) { - parsedStyle = { - "border-width": "1px", - 'font-size': '0px', - } - } - - event.target.animate({ - style: parsedStyle, - }, { - duration: animationDuration, - }) - } - - const buttonColor = "rgba(255,255,255,0.9)" - const buttonBackgroundColor = "#1f2023" - const addStartnodeButton = (event) => { - var parentNode = cy.$('#' + event.target.data("id")); - if (parentNode.data('isButton') || parentNode.data('buttonId')) - return - - if (parentNode.data("isStartNode")) { - return - } - - //parentNode.lock() - const px = parentNode.position('x') - 65 - const py = parentNode.position('y') - 45 - const circleId = newNodeId = uuidv4() - - parentNode.data('circleId', circleId) - - const iconInfo = { - "icon": "M9.4 2H15V12H8L7.6 10H2V17H0V0H9L9.4 2ZM9 10H11V8H13V6H11V4H9V6L8 4V2H6V4H4V2H2V4H4V6H2V8H4V6H6V8H8V6L9 8V10ZM6 6V4H8V6H6ZM9 6H11V8H9V6Z", - "iconColor": buttonColor, - "iconBackgroundColor": buttonBackgroundColor, - } - - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - cy.add({ - group: 'nodes', - data: { - weight: 30, - id: circleId, - name: "TEEEXT", - isButton: true, - buttonType: "set_startnode", - attachedTo: event.target.data("id"), - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - is_valid: true, - }, - position: { x: px, y: py }, - locked: true - }) - //.unselectify() - } - - const addCopyButton = (event) => { - var parentNode = cy.$('#' + event.target.data("id")); - if (parentNode.data('isButton') || parentNode.data('buttonId')) - return - - //parentNode.lock() - const px = parentNode.position('x') - 65 - const py = parentNode.position('y') - 5 - const circleId = newNodeId = uuidv4() - - parentNode.data('circleId', circleId) - - const iconInfo = { - "icon": "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", - "iconColor": buttonColor, - "iconBackgroundColor": buttonBackgroundColor, - } - - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - cy.add({ - group: 'nodes', - data: { - weight: 30, - id: circleId, - name: "TEEEXT", - isButton: true, - buttonType: "copy", - attachedTo: event.target.data("id"), - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - is_valid: true, - }, - position: { x: px, y: py }, - locked: true - }) - //.unselectify() - } - - const addDeleteButton = (event) => { - var parentNode = cy.$('#' + event.target.data("id")); - if (parentNode.data('isButton') || parentNode.data('buttonId')) - return - - //parentNode.lock() - const px = parentNode.position('x') - 65 - const py = parentNode.position('y') + 35 - const circleId = newNodeId = uuidv4() - - parentNode.data('circleId', circleId) - - const iconInfo = { - "icon": "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", - "iconColor": buttonColor, - "iconBackgroundColor": buttonBackgroundColor, - } - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - cy.add({ - group: 'nodes', - data: { - weight: 30, - id: circleId, - name: "TEEEXT", - isButton: true, - buttonType: "delete", - attachedTo: event.target.data("id"), - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - is_valid: true, - }, - position: { x: px, y: py }, - locked: true - }) - //.unselectify() - } - - const onNodeHover = (event) => { - //console.log("TAR: ", event.target) - const nodedata = event.target.data() - if (nodedata.finished === false) { - return - } - - var parentNode = cy.$('#' + event.target.data("id")); - if (parentNode.data('isButton') || parentNode.data('buttonId')) - return - - if (nodedata.app_name !== undefined) { - const allNodes = cy.nodes().jsons() - - var found = false - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { - cy.getElementById(currentNode.data.id).remove() - } - - if (currentNode.data.isButton && currentNode.data.attachedTo === nodedata.id) { - found = true - } - } - - if (!found) { - addDeleteButton(event) - addCopyButton(event) - addStartnodeButton(event) - } - } - - - const parsedStyle = { - "border-width": "7px", - "border-opacity": ".7", - 'font-size': '25px', - 'color': 'white', - } - - event.target.animate({ - style: parsedStyle - }, { - duration: animationDuration, - }) - - previousnodecolor = event.target.style("border-color") - } - - const onEdgeHoverOut = (event) => { - if (event === null || event === undefined) { - event.target.removeStyle() - return - } - - const edgeData = event.target.data() - if (edgeData.decorator === true) { - return - } - - event.target.removeStyle() - } - - // This is here to have a proper transition for lines - const onEdgeHover = (event) => { - if (event === null || event === undefined) { - return - } - - const edgeData = event.target.data() - if (edgeData.decorator === true) { - return - } - - const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color") - const targetcolor = cy.getElementById(event.target.data("target")).style("border-color") - if (sourcecolor !== null && sourcecolor !== undefined && targetcolor !== null && targetcolor !== undefined) { - event.target.animate({ - style: { - 'target-arrow-color': targetcolor, - "line-fill": "linear-gradient", - "line-gradient-stop-colors": [sourcecolor, targetcolor], - "line-gradient-stop-positions": [0, 1], - }, - duration: 0, - }) - } - } - - const setupGraph = () => { - const actions = workflow.actions.map(action => { - const node = {} - - if (!action.isStartNode && action.app_name === "Shuffle Tools") { - const iconInfo = GetIconInfo(action) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - action.large_image = svgpin_Url - action.fillGradient = iconInfo.fillGradient - action.fillstyle = "solid" - if (action.fillGradient !== undefined && action.fillGradient !== null && action.fillGradient.length > 0) { - action.fillstyle = 'linear-gradient' - //console.log("GRADIENT!: ", action) - //action.fillstyle = - //'background-fill': 'data(fillstyle)', - } else { - action.iconBackground = iconInfo.iconBackgroundColor - } - //console.log("FOUND NODE INFO: ", action) - } - - node.position = action.position - node.data = action - - node.data._id = action["id"] - node.data.type = "ACTION" - node.isStartNode = action["id"] === workflow.start - - - var example = "" - if (action.example !== undefined && action.example !== null && action.example.length > 0) { - example = action.example - } - - node.data.example = example - - return node; - }) - - const decoratorNodes = workflow.actions.map(action => { - if (!action.isStartNode) { - if (action.app_name === "Testing") { - return null - } else if (action.app_name === "Shuffle Tools") { - return null - } - } - - const iconInfo = GetIconInfo(action) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - - const offset = action.isStartNode ? 36 : 44 const decoratorNode = { position: { - x: action.position.x+offset, - y: action.position.y+offset, + x: event.target.position().x + 0, + y: event.target.position().y + 65, }, locked: true, data: { - "isDescriptor": true, - "isValid": true, - "is_valid": true, - "label": "", - "image": svgpin_Url, - "imageColor": iconInfo.iconBackgroundColor, - "attachedTo": action.id, - }, - } - return decoratorNode - return null - }) - - const triggers = workflow.triggers.map(trigger => { - const node = {} - node.position = trigger.position - node.data = trigger - - node.data._id = trigger["id"] - node.data.type = "TRIGGER" - - return node; - }) - - // FIXME - tmp branch update - var insertedNodes = [].concat(actions, triggers, decoratorNodes) - insertedNodes = insertedNodes.filter(node => node !== null) - - var edges = workflow.branches.map((branch, index) => { - //workflow.branches[index].conditions = [{ - - const edge = { }; - var conditions = workflow.branches[index].conditions - if (conditions === undefined || conditions === null) { - conditions = [] - } - - var label = "" - if (conditions.length === 1) { - label = conditions.length+" condition" - } else if (conditions.length > 1) { - label = conditions.length+" conditions" - } - - edge.data = { - id: branch.id, - _id: branch.id, - source: branch.source_id, - target: branch.destination_id, - label: label, - conditions: conditions, - hasErrors: branch.has_errors, - decorator: false, - } - - // This is an attempt at prettier edges. The numbers are weird to work with. - /* - //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html - const sourcenode = actions.find(node => node.data._id === branch.source_id) - const destinationnode = actions.find(node => node.data._id === branch.destination_id) - if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { - //node.data._id = action["id"] - console.log("SOURCE: ", sourcenode.position) - console.log("DESTINATIONNODE: ", destinationnode.position) - - var opposite = true - if (sourcenode.position.x > destinationnode.position.x) { - opposite = false - } else { - opposite = true - } - - edge.style = { - 'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"], - 'control-point-weight': ['0.3', '0.7'], - } - } - */ - - return edge; - }) - - if (workflow.visual_branches !== undefined && workflow.visual_branches !== null && workflow.visual_branches.length > 0) { - const visualedges = workflow.visual_branches.map((branch, index) => { - const edge = { }; - - if (workflow.branches[index] === undefined) { - return {} - } - - var conditions = workflow.branches[index].conditions - if (conditions === undefined || conditions === null) { - conditions = [] - } - - const label = "Subflow" - edge.data = { - id: branch.id, - _id: branch.id, - source: branch.source_id, - target: branch.destination_id, - label: label, - decorator: true, - } - - return edge; - }) - - edges = edges.concat(visualedges) - } - - setWorkflow(workflow) - - // Verifies if a branch is valid and skips others - var newedges = [] - for (var key in edges) { - var item = edges[key] - if (item.data === undefined) { - continue - } - - const sourcecheck = insertedNodes.find(data => data.data.id === item.data.source) - const destcheck = insertedNodes.find(data => data.data.id === item.data.target) - if (sourcecheck === undefined || destcheck === undefined) { - continue - } - - newedges.push(item) - } - - insertedNodes = insertedNodes.concat(newedges) - setElements(insertedNodes) - } - - const removeNode = () => { - setSelectedApp({}) - setSelectedAction({}) - - const selectedNode = cy.$(':selected') - if (selectedNode.data() === undefined) { - return - } - - if (selectedNode.data().type === "TRIGGER") { - console.log("Should remove trigger!") - console.log(selectedNode.data()) - const triggerindex = workflow.triggers.findIndex(data => data.id === selectedNode.data().id) - setSelectedTriggerIndex(triggerindex) - if (selectedNode.data().trigger_type === "SCHEDULE") { - setSelectedTrigger(selectedNode.data()) - stopSchedule(selectedNode.data(), triggerindex) - } else if (selectedNode.data().trigger_type === "WEBHOOK") { - setSelectedTrigger(selectedNode.data()) - deleteWebhook(selectedNode.data(), triggerindex) - } else if (selectedNode.data().trigger_type === "EMAIL") { - setSelectedTrigger(selectedNode.data()) - stopMailSub(selectedTrigger, triggerindex) - } - - } - - if (selectedNode.data().decorator === true) { - alert.info("This edge can't be deleted.") - } else { - selectedNode.remove() - } - - setSelectedTrigger({}) - setSelectedTriggerIndex({}) - } - - const stopSchedule = (trigger, triggerindex) => { - //alert.info("Stopping schedule") - fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule/"+trigger.id, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for stream results :O!") - } - - return response.json() - }) - .then((responseJson) => { - // No matter what, it's being stopped. - if (!responseJson.success) { - if (responseJson.reason !== undefined) { - alert.error("Failed to stop schedule: " + responseJson.reason) - } - } else { - alert.success("Successfully stopped schedule") - } - - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setSelectedTrigger(trigger) - setWorkflow(workflow) - saveWorkflow(workflow) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - //const submitSchedule = (id, name, frequency, executionArg) => { - const submitSchedule = (trigger, triggerindex) => { - //const cronSplit = workflow.triggers[triggerindex].parameters[0].value.split("*") - //if (cronSplit.length <= 5 || cronSplit.length > 6) { - // alert.error("Error: Bad cron, example run every 1 minute: */1 * * * *") - // return - //} - - if (trigger.name.length <= 0) { - alert.error("Error: name can't be empty") - return - } - - alert.info("Attempting to create schedule with name " + trigger.name) - const data = { - "name": trigger.name, - "frequency": workflow.triggers[triggerindex].parameters[0].value, - "execution_argument": workflow.triggers[triggerindex].parameters[1].value, - "environment": workflow.triggers[triggerindex].environment, - "id": trigger.id, - } - - fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(data), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for stream results :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success) { - alert.error("Failed to set schedule: " + responseJson.reason) - } else { - alert.success("Successfully created schedule") - workflow.triggers[triggerindex].status = "running" - trigger.status = "running" - setSelectedTrigger(trigger) - setWorkflow(workflow) - console.log("Should set the status to running and save") - saveWorkflow(workflow) - } - }) - .catch(error => { - alert.error(error.toString()) - }); - } - - const appViewStyle = { - marginLeft: 5, - marginRight: 5, - display: "flex", - flexDirection: "column", - height: "100%", - } - - - const paperAppStyle = { - borderRadius: theme.palette.borderRadius, - minHeight: 100, - maxHeight: 100, - minWidth: "100%", - maxWidth: "100%", - marginTop: "5px", - color: "white", - backgroundColor: surfaceColor, - cursor: "pointer", - display: "flex", - } - - const paperVariableStyle = { - borderRadius: theme.palette.borderRadius, - minHeight: 50, - maxHeight: 50, - minWidth: "100%", - maxWidth: "100%", - marginTop: "5px", - color: "white", - backgroundColor: surfaceColor, - cursor: "pointer", - display: "flex", - } - - const VariablesView = () => { - const [open, setOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - - const menuClick = (event) => { - setOpen(!open) - setAnchorEl(event.currentTarget); - } - - const deleteVariable = (variableName) => { - console.log("Delete:" ,variableName) - workflow.workflow_variables = workflow.workflow_variables.filter(data => data.name !== variableName) - setWorkflow(workflow) - } - - const deleteExecutionVariable = (variableName) => { - workflow.execution_variables = workflow.execution_variables.filter(data => data.name !== variableName) - setWorkflow(workflow) - } - - const variableScrollStyle = { - margin: 15, - overflow: "scroll", - height: "66vh", - overflowX: "auto", - overflowY: "auto", - flex: "10", - } - - return ( -
    -
    - What are WORKFLOW variables? - {workflow.workflow_variables === null ? - null : workflow.workflow_variables.map((variable, index) => { - return ( -
    - { - }}> -
    -
    -
    { - setNewVariableName(variable.name) - setNewVariableDescription(variable.description) - setNewVariableValue(variable.value) - setVariablesModalOpen(true)}}> - Name: {variable.name} -
    -
    - - - - { - setOpen(false) - setAnchorEl(null) - }} - > - - { - setOpen(false) - setNewVariableName(variable.name) - setNewVariableDescription(variable.description) - setNewVariableValue(variable.value) - setVariablesModalOpen(true) - }} key={"Edit"}>{"Edit"} - { - deleteVariable(variable.name) - setOpen(false) - }} key={"Delete"}>{"Delete"} - -
    -
    - -
    - ) - })} -
    - -
    - - What are EXECUTION variables? - {workflow.execution_variables === null || workflow.execution_variables === undefined ? - null : workflow.execution_variables.map(variable=> { - return ( -
    - { - }}> -
    -
    -
    { - setNewVariableName(variable.name) - setExecutionVariablesModalOpen(true) - }}> - Name: {variable.name} -
    -
    - - - - { - setOpen(false) - setAnchorEl(null) - }} - > - - { - setOpen(false) - setNewVariableName(variable.name) - setExecutionVariablesModalOpen(true) - }} key={"Edit"}>{"Edit"} - { - deleteExecutionVariable(variable.name) - setOpen(false) - }} key={"Delete"}>{"Delete"} - -
    -
    - -
    - ) - })} -
    - -
    -
    -
    - ) - } - - const curTab = 0 - const handleSetTab = (event, newValue) => { - setCurrentView(newValue) - } - - const HandleLeftView = () => { - // Defaults to apps. - var thisview = - if (currentView === 1) { - thisview = - } else if (currentView === 2) { - thisview = - } - - const tabStyle = { - maxWidth: leftBarSize/3, - minWidth: leftBarSize/3, - flex: 1, - textTransform: "none", - } - - const iconStyle = { - marginTop: 3, - marginRight: 5, - } - - return( -
    -
    - {thisview} -
    - - - - - - - - Apps - - - } style={tabStyle} /> - - - - - - Triggers - - - } style={tabStyle} /> - - - - - - Variables - - - }style={tabStyle} /> - -
    - ) - } - - const triggers = [{ - "name": "Webhook", - "type": "TRIGGER", - "status": "uninitialized", - "trigger_type": "WEBHOOK", - "errors": null, - "large_image": 'data:image/png;base64,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', - "is_valid": true, - "label": "Webhook", - "environment": "onprem", - "description": "Simple HTTP webhook", - "long_description": "Execute a workflow with an unauthicated POST request", - }, - { - "name": "Schedule", - "type": "TRIGGER", - "status": "uninitialized", - "description": "Schedule execution time", - "trigger_type": "SCHEDULE", - "errors": null, - "large_image": "data:image/png;base64,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", - "label": "Schedule", - "is_valid": true, - "environment": "onprem", - "long_description": "Create a schedule based on cron", - }, - { - "name": "Shuffle Workflow", - "type": "TRIGGER", - "status": "uninitialized", - "trigger_type": "SUBFLOW", - "errors": null, - "large_image": 'data:image/png;base64,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', - "is_valid": true, - "label": "Subflow", - "environment": "onprem", - "description": "Control another workflow", - "long_description": "Execute another workflow from this workflow", - }, - { - "name": "User Input", - "type": "TRIGGER", - "status": "running", - "large_image": "data:image/png;base64,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", - "description": "Wait for user input", - "trigger_type": "USERINPUT", - "errors": null, - "is_valid": cloudSyncEnabled || isCloud ? true : false, - "label": "User input", - "environment": "cloud", - "long_description": "Take user input to continue execution", - }, - { - "name": "Office365", - "type": "TRIGGER", - "status": "uninitialized", - "description": "Starts upon O365 email", - "trigger_type": "EMAIL", - "errors": null, - "is_valid": cloudSyncEnabled || isCloud ? true : false, - "label": "Email", - "environment": "cloud", - "large_image": 'data:image/jpg;base64,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', - "long_description": "Execute a workflow when you get an email", - }, - { - "name": "Gmail", - "type": "TRIGGER", - "status": "uninitialized", - "description": "Trigger based on Gmail", - "trigger_type": "EMAIL", - "errors": null, - "is_valid": cloudSyncEnabled || isCloud ? true : false, - "label": "Email", - "environment": "cloud", - "large_image": 'data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAzFodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QTIyMjgyMEYwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QTIyMjgyMTAwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozQTMwMDQxRTAwMEUxMUVCQkE4QTk5QkEzQzMxMDZEMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBMjIyODIwRTAwMkMxMUVCQkE4QTk5QkEzQzMxMDZEMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAwICAwMCAwMDAwMEBwUEBAQECQYHBQcKCQsL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', - "long_description": "Execute a workflow when you get an email", - } - ] - - const TriggersView = () => { - const triggersViewStyle = { - marginLeft: "10px", - marginRight: "10px", - display: "flex", - flexDirection: "column", - } - - // Predefined hurr - - return ( -
    -
    - {triggers.map((trigger, index) => { - var imageline = trigger.large_image.length === 0 ? - - : - - - const color = trigger.is_valid ? green : yellow - return( - {handleTriggerDrag(e, trigger)}} - onStop={(e) => { - handleDragStop(e) - }} - dragging={false} - position={{ - x: 0, - y: 0, - }} - > - {}}> -
    -
    - - - - {imageline} - - - - -

    {trigger.name}

    -
    - - {trigger.description} - -
    -
    -
    -
    - ) - })} -
    -
    - ) - } - - var newNodeId = "" - var parsedApp = {} - const handleTriggerDrag = (e, data) => { - const cycontainer = cy.container() - // Chrome lol - //if (e.srcElement !== undefined && e.srcElement.localName === "canvas") { - if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) { - if (newNodeId.length > 0) { - var currentnode = cy.getElementById(newNodeId) - if (currentnode.length === 0) { - return - } - - currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft) - currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop) - } else{ - if (workflow.start === "" || workflow.start === undefined) { - alert.error("Define a starting action first.") - return - } - - //if (data.is_valid === false) { - // alert.error(data.name+" requires hybrid version of Shuffle") - // return - //} - - const triggerLabel = getNextActionName(data.name) - - newNodeId = uuidv4() - const newposition = { - "x": e.pageX-cycontainer.offsetLeft, - "y": e.pageY-cycontainer.offsetTop, - } - - const newAppData = { - app_name: data.name, - app_version: "1.0.0", - environment: isCloud ? "cloud" : data.environment, - description: data.description, - long_description: data.long_description, - errors: [], - id_: newNodeId, - _id_: newNodeId, - id: newNodeId, - finished: false, - label: triggerLabel, - type: data.type, + isButton: true, + isValid: true, is_valid: true, - trigger_type: data.trigger_type, - large_image: data.large_image, - status: "uninitialized", - name: data.name, - isStartNode: false, - position: newposition, - } + //label: "+", + attachedTo: nodedata.id, + imageColor: color, + buttonType: "suggestion", + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + }, + }; - //if (data.trigger_type === "WEBHOOK") { - // newAppData.status = "running" - //} - - // Can all the data be in here? hmm - const nodeToBeAdded = { - group: "nodes", - data: newAppData, - renderedPosition: newposition, - } - - cy.add(nodeToBeAdded) - parsedApp = nodeToBeAdded - return - } + cy.add(decoratorNode); } - } - - const handleDragStop = (e, app) => { - //console.log("STOP!: ", e) - //console.log("APP!: ", parsedApp) - //const onNodeAdded = (event) => { - //const node = event.target - //const nodedata = event.target.data() - var currentnode = cy.getElementById(newNodeId) - if (currentnode === undefined || currentnode === null || currentnode.length === 0) { + + //setWorkflowRecommendations(responseJson.actions) + if (workflowRecommendations.length === 0) { return } - // Using remove & replace, as this triggers the function - // onNodeAdded() with this node after it's added - - currentnode.remove() - parsedApp.data.finished = true - parsedApp.data.position = currentnode.renderedPosition() - parsedApp.position = currentnode.renderedPosition() - parsedApp.renderedPosition = currentnode.renderedPosition() + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - var newAppData = parsedApp.data - if (newAppData.type === "ACTION") { - // AUTHENTICATION - if (app.authentication.required) { - // Setup auth here :) - const authenticationOptions = [] - var findAuthId = "" - if (newAppData.authentication_id !== null && newAppData.authentication_id !== undefined && newAppData.authentication_id.length > 0) { - findAuthId = newAppData.authentication_id - } + const px = parentNode.position("x") + 300; + const py = parentNode.position("y") + -100; + const circleId = (newNodeId = uuidv4()); - var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) - for (var key in tmpAuth) { - var item = tmpAuth[key] + parentNode.data("circleId", circleId); - const newfields = {} - for (var filterkey in item.fields) { - newfields[item.fields[filterkey].key] = item.fields[filterkey].value - } - - item.fields = newfields - if (item.app.name === app.name) { - authenticationOptions.push(item) - if (item.id === findAuthId) { - newAppData.selectedAuthentication = item - } - } - } - - if (authenticationOptions !== undefined && authenticationOptions !== null && authenticationOptions.length > 0) { - for (var key in authenticationOptions) { - const option = authenticationOptions[key] - if (option.active) { - newAppData.selectedAuthentication = option - newAppData.authentication_id = option.id - break - } - } - } - - //newAppData.authentication = authenticationOptions - //if (newAppData.selectedAuthentication === null || newAppData.selectedAuthentication === undefined || newAppData.selectedAuthentication.length === "") { - // newAppData.selectedAuthentication = {} - //} else { - // console.log("CAN WE SELECT AUTH?: ", authenticationOptions) - //} - // - // - - //console.log(parsedApp) - } else { - newAppData.authentication = [] - newAppData.authentication_id = "" - newAppData.selectedAuthentication = {} + var startHeight = 0 + for (let recKey in workflowRecommendations) { + const rec = workflowRecommendations[recKey] + if (rec.action_id !== nodedata.id) { + continue } - parsedApp.data = newAppData - cy.add(parsedApp) - } else if (newAppData.type === "TRIGGER") { - cy.add(parsedApp) + if (rec.recommendations === undefined || rec.recommendations.length === 0) { + continue + } - } + for (let recIndex in rec.recommendations) { + const parsedRec = rec.recommendations[recIndex] + startHeight += 100 - newNodeId = "" - parsedApp = {} - } - - const appScrollStyle = { - overflow: "scroll", - maxHeight: bodyHeight-appBarSize-55-50, - minHeight: bodyHeight-appBarSize-55-50, - marginTop: 1, - overflowY: "auto", - overflowX: "hidden", - } - - const handleAppDrag = (e, app) => { - const cycontainer = cy.container() - - // Chrome lol - //if (e.srcElement !== undefined && e.srcElement.localName === "canvas") { - if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) { - if (newNodeId.length > 0) { - var currentnode = cy.getElementById(newNodeId) - if (currentnode === undefined || currentnode === null || currentnode.length === 0) { - return - } - - currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft) - currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop) - } else { - if (workflow.public) { - console.log("workflow is public - not adding") - return - } - - if (app.actions === undefined || app.actions === null || app.actions.length === 0) { - alert.error("App "+app.name+" currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.") - return - } - - newNodeId = uuidv4() - const actionType = "ACTION" - const actionLabel = getNextActionName(app.name) - var parameters = null - var example = "" - - if (app.actions[0].parameters !== null && app.actions[0].parameters.length > 0) { - parameters = app.actions[0].parameters - } - if (app.actions[0].returns.example !== undefined && app.actions[0].returns.example !== null && app.actions[0].returns.example.length > 0) { - example = app.actions[0].returns.example - } + const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" + const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : theme.palette.defaultImage - const parsedEnvironments = environments === null || environments === [] ? "cloud" : environments[defaultEnvironmentIndex] === undefined ? "cloud" : environments[defaultEnvironmentIndex].Name - const newAppData = { - app_name: app.name, - app_version: app.app_version, - app_id: app.id, - sharing: app.sharing, - private_id: app.private_id, - environment: parsedEnvironments, - errors: [], - id_: newNodeId, - _id_: newNodeId, - id: newNodeId, - is_valid: true, - label: actionLabel, - type: actionType, - name: app.actions[0].name, - parameters: parameters, - isStartNode: false, - large_image: app.large_image, - authentication: [], - execution_variable: undefined, - example: example, - category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "", - authentication_id: "", - finished: false, - } - // FIXME: overwrite category if the ACTION chosen has a different category + var newapp = { + app_name: parsedRec.app_name, + app_version: foundVersion, + app_id: parsedRec.app_id, + sharing: false, + private_id: false, + isStartNode: false, + label: "Suggestion "+recIndex, + large_image: largeImage, + finished: false, + is_valid: true, + isSuggestion: true, + attachedTo: event.target.data("id"), + } - // const image = "url("+app.large_image+")" - // FIXME - find the cytoscape offset position - // Can this be done with zoom calculations? - const nodeToBeAdded = { - group: "nodes", - data: newAppData, - renderedPosition: { - //x: e.layerX, - //y: e.layerY, - x: e.pageX-cycontainer.offsetLeft, - y: e.pageY-cycontainer.offsetTop, + cy.add({ + group: "nodes", + data: newapp, + position: { + x: px, + y: py+startHeight, + }, + locked: true, + }); + } + console.log("Got Rec: ", rec) + } + } + + const addDeleteButton2 = (event) => { + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + + const px = parentNode.position("x") + 100; + const py = parentNode.position("y") + 35; + const circleId = (newNodeId = uuidv4()); + + parentNode.data("circleId", circleId); + + const iconInfo = { + icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + cy.add({ + group: "nodes", + data: { + weight: 30, + id: circleId, + name: "This is autocomplete", + buttonType: "delete", + attachedTo: event.target.data("id"), + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + is_valid: true, + }, + position: { x: px, y: py }, + locked: true, + }); + }; + + const addDeleteButton = (event) => { + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + + const px = parentNode.position("x") - 65; + const py = parentNode.position("y") + 35; + const circleId = (newNodeId = uuidv4()); + + parentNode.data("circleId", circleId); + + const iconInfo = { + icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + cy.add({ + group: "nodes", + data: { + weight: 30, + id: circleId, + name: "TEEEXT", + isButton: true, + buttonType: "delete", + attachedTo: event.target.data("id"), + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + is_valid: true, + }, + position: { x: px, y: py }, + locked: true, + }); + }; + + const onNodeHover = (event) => { + const nodedata = event.target.data(); + + if (nodedata.finished === false) { + console.log("NODE: ", nodedata) + return; + } + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "pointer" + } + + //var parentNode = cy.$("#" + event.target.data("id")); + //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + + if (nodedata.app_name !== undefined && !workflow.public === true) { + const allNodes = cy.nodes().jsons(); + + if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { + console.log("In this :)") + + var found = false; + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if ( + currentNode.data.attachedTo === nodedata.id && + currentNode.data.isDescriptor + ) { + found = true; + console.log("FOUND THE NODE!"); + break; } } - parsedApp = nodeToBeAdded - cy.add(nodeToBeAdded) - return + 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: { + x: event.target.position().x + 44, + y: event.target.position().y + 44, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + isTrigger: true, + label: `${matchingExecutions.length}`, + attachedTo: nodedata.id, + imageColor: color, + hasExecutions: true, + }, + }; + + cy.add(decoratorNode) } } + + var found = false; + for (var _key in allNodes) { + const currentNode = allNodes[_key]; + // console.log("CURRENT NODE: ", currentNode) + if ( + (currentNode.data.isButton || currentNode.data.isSuggestion) && + currentNode.data.attachedTo !== nodedata.id + ) { + cy.getElementById(currentNode.data.id).remove(); + } + + /*if ( + currentNode.data.isSuggestion && + currentNode.data.attachedTo !== nodedata.id + ) { + cy.getElementById(currentNode.data.id).remove(); + }*/ + + if ( + currentNode.data.isButton && + currentNode.data.attachedTo === nodedata.id + ) { + found = true; + } + } + + if (!found) { + addDeleteButton(event); + + if (nodedata.type === "TRIGGER") { + if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { + addCopyButton(event); + } else { + // Check how many executions from the source + addRunCountButton(event); + } + } else { + addCopyButton(event); + addStartnodeButton(event); + } + + // autocomplete + // right click + // suggestions + addSuggestionButtons(nodedata, event); + } + } + + var parsedStyle = { + "border-width": "7px", + "border-opacity": ".7", + "font-size": "25px", + //"cursor": "pointer", + } + + const typeIds = cy.elements('node:selected').jsons(); + for (var idkey in typeIds) { + const item = typeIds[idkey] + if (item.data.id === nodedata.id) { + //console.log("items: ", item.data.id, nodedata.id) + parsedStyle["border-width"] = "12px" + break + } + } + + if (nodedata.type !== "COMMENT") { + parsedStyle.color = "white"; + + //if (!event.target.data("isButton") && !event.target.data("buttonId")) { + // const px = event.target.position("x") - 0; + // const py = event.target.position("y") - 50; + // const circleId = (newNodeId = uuidv4()); + + // console.log("Got px, py: ", px, py) + // + // cy.add({ + // group: "nodes", + // data: { + // weight: 30, + // id: circleId, + // isButton: true, + // attachedTo: event.target.data("id"), + // buttonType: "edgehandler", + // is_valid: true, + // }, + // position: { x: px, y: py }, + // locked: true, + // }) + //} + } + + if (event.target !== undefined && event.target !== null) { + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ); + } + + const outgoingEdges = event.target.outgoers("edge"); + const incomingEdges = event.target.incomers("edge"); + if (outgoingEdges.length > 0) { + outgoingEdges.addClass("hover-highlight"); + } + + if (incomingEdges.length > 0) { + outgoingEdges.addClass("hover-highlight"); + } + }; + + const onEdgeHoverOut = (event) => { + if (event === null || event === undefined) { + event.target.removeStyle(); + return; + } + + const edgeData = event.target.data(); + if (edgeData.decorator === true) { + return; + } + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "default" + } + + //event.target.removeStyle(); + }; + + // This is here to have a proper transition for lines + const onEdgeHover = (event) => { + if (event === null || event === undefined) { + return; + } + + const edgeData = event.target.data(); + if (edgeData.decorator === true) { + return; + } + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "pointer" + } + + //const sourcecolor = cy + // .getElementById(event.target.data("source")) + // .style("border-color"); + //const targetcolor = cy + // .getElementById(event.target.data("target")) + // .style("border-color"); + + ////console.log(sourcecolor, targetcolor) + //if ( + // sourcecolor !== null && + // sourcecolor !== undefined && + // targetcolor !== null && + // targetcolor !== undefined && + // !sourcecolor.includes("rgb") && + // !targetcolor.includes("rgb") + //) { + // console.log(sourcecolor) + // console.log(targetcolor) + + // if (event.target !== null && event.target.value !== null) { + // event.target.animate({ + // style: { + // "target-arrow-color": targetcolor, + // "line-fill": "linear-gradient", + // "line-gradient-stop-colors": [sourcecolor, targetcolor], + // "line-gradient-stop-positions": [0, 1], + // }, + // duration: animationDuration, + // }) + // } else { + // event.target.animate({ + // style: { + // "target-arrow-color": targetcolor, + // "line-fill": "linear-gradient", + // "line-gradient-stop-colors": ["#41dcab", "#41dcab"], + // "line-gradient-stop-positions": [0, 1], + // }, + // duration: animationDuration, + // }) + + // } + //} + + if (event.target !== undefined && event.target !== null) { + //const targetcolor = "#66a8b1" + //const parsedStyle = { + // "width": "10px", + // "font-size": "18px", + // "target-arrow-color": "#66a8b1", + // "color": "#66a8b1", + //} + + //event.target.addClass("shuffle-hover-highlight"); + + //console.log("Style1: ", event.target) + //console.log("Style: ", event.target.style()) + + //event.target.animate( + // { + // style: parsedStyle, + // }, + // { + // duration: animationDuration, + // } + //) + } + } + + // Thanks to: + // Calculates how a branch should curve (it's still weird~) + // https://codepen.io/guillaumethomas/pen/xxbbBKO + const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => { + const xParsed = destinationnodePosition.x - sourcenodePosition.x + const yParsed = destinationnodePosition.y - sourcenodePosition.y + + const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed); + const costheta = xParsed / z; + const alpha = 0.25; + var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta]; + var controlPointWeight = [alpha, 1 - alpha] + + //'control-point-weight': ['0.33', '0.66'], + //var controlPointWeight = ["0.33", "0.66"] + //var controlPointDistance = ["33%", "-66%"] + //var controlPointWeight = ["0.00", "1.00"] + /* + if (yParsed !== 0) { + //const degreeFound = Math.atan2(xParsed / yParsed) + const degreeFound = Math.atan2(xParsed, yParsed) * 180 / Math.PI + + if (degreeFound > 90 && degreeFound < 180) { + console.log("TOPRIGHT") + } else if (degreeFound < 90 && degreeFound > 0) { + console.log("BOTTOMRIGHT") + } else if (degreeFound < 0 && degreeFound > -90) { + console.log("BOTTOMLEFT") + //controlPointWeight = ["0.20", "0.80"] + //controlPointWeight = "0.7" + //controlPointDistance = "50%" + + } else if (degreeFound < -90 && degreeFound > -180) { + console.log("TOPLEFT") + } else { + console.log("STRAIGHT!") + } + } + */ + if (isNaN(controlPointDistance[0])) { + controlPointDistance[0] = 0 + } + if (isNaN(controlPointDistance[1])) { + controlPointDistance[1] = 0 } - const AppView = (props) => { - const { allApps, prioritizedApps, filteredApps } = props; - const [visibleApps, setVisibleApps] = React.useState(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)))) + if (isNaN(controlPointWeight[0])) { + controlPointWeight[0] = 0 + } + if (isNaN(controlPointWeight[1])) { + controlPointWeight[1] = 0 + } - const ParsedAppPaper = (props) => { - const app = props.app - const [hover, setHover] = React.useState(false) + return { + "distance": controlPointDistance, + "weight": controlPointWeight, + } + } - const maxlen = 24 - var newAppname = app.name - newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) - if (newAppname.length > maxlen) { - newAppname = newAppname.slice(0, maxlen)+".." + const setupGraph = () => { + const actions = workflow.actions.map((action) => { + const node = {}; + + if (!action.isStartNode && action.app_name === "Shuffle Tools") { + const iconInfo = GetIconInfo(action); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + action.large_image = svgpin_Url; + action.fillGradient = iconInfo.fillGradient; + action.fillstyle = "solid"; + if ( + action.fillGradient !== undefined && + action.fillGradient !== null && + action.fillGradient.length > 0 + ) { + action.fillstyle = "linear-gradient"; + } else { + action.iconBackground = iconInfo.iconBackgroundColor; + } + } + + node.position = action.position; + node.data = action; + + node.data.id = action["id"]; + node.data._id = action["id"]; + node.data.type = "ACTION"; + node.isStartNode = action["id"] === workflow.start; + + if (workflow.public === true) { + node.data.is_valid = true + node.is_valid = true + } + + var example = ""; + if ( + action.example !== undefined && + action.example !== null && + action.example.length > 0 + ) { + example = action.example; + } + + node.data.example = example; + + return node; + }); + + const decoratorNodes = workflow.actions.map((action) => { + if (!action.isStartNode) { + if (action.app_name === "Testing") { + return null; + } else if (action.app_name === "Shuffle Tools") { + return null; + } + } + + const iconInfo = GetIconInfo(action); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const offset = action.isStartNode ? 36 : 44; + const decoratorNode = { + position: { + x: action.position.x + offset, + y: action.position.y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: action.id, + }, + }; + return decoratorNode; + }); + + + const foundtriggers = workflow.triggers.map((trigger) => { + const node = {}; + node.position = trigger.position; + node.data = trigger; + + node.data._id = trigger["id"]; + node.data.id = trigger["id"]; + node.data.type = "TRIGGER"; + + return node; + }); + + var comments = []; + if ( + workflow.comments !== undefined && + workflow.comments !== null && + workflow.comments.length > 0 + ) { + comments = workflow.comments.map((comment) => { + const node = {}; + node.position = comment.position; + node.data = comment; + + node.data._id = comment["id"]; + node.data.type = "COMMENT"; + + return node; + }); + } + + // FIXME - tmp branch update + var insertedNodes = [].concat(actions, foundtriggers, decoratorNodes, comments); + insertedNodes = insertedNodes.filter((node) => node !== null); + + var edges = workflow.branches.map((branch, index) => { + const edge = {}; + var conditions = workflow.branches[index].conditions; + if (conditions === undefined || conditions === null) { + conditions = []; + } + + var label = ""; + if (conditions.length === 1) { + label = conditions.length + " condition"; + } else if (conditions.length > 1) { + label = conditions.length + " conditions"; + } + + edge.data = { + id: branch.id, + _id: branch.id, + source: branch.source_id, + target: branch.destination_id, + label: label, + conditions: conditions, + hasErrors: branch.has_errors, + decorator: false, + }; + + // This is an attempt at prettier edges. The numbers are weird to work with. + // Bezier curves + //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html + var sourcenode = actions.find(node => node.data._id === branch.source_id || node.data.id === branch.source_id) + var destinationnode = actions.find(node => node.data._id === branch.destination_id || node.data.id === branch.destination_id) + + if (sourcenode === undefined) { + sourcenode = foundtriggers.find(node => node.data._id === branch.source_id || node.data.id === branch.source_id) + } + + if (destinationnode === undefined) { + destinationnode = foundtriggers.find(node => node.data._id === branch.destination_id || node.data.id === branch.destination_id) + } + + if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { + const edgeCurve = calculateEdgeCurve(sourcenode.position, destinationnode.position) + edge.style = { + 'control-point-distance': edgeCurve.distance, + 'control-point-weight': edgeCurve.weight, + } + } else { + console.log("FAILED node curve handling") + } + + return edge; + }); + + if ( + workflow.visual_branches !== undefined && + workflow.visual_branches !== null && + workflow.visual_branches.length > 0 + ) { + const visualedges = workflow.visual_branches.map((branch, index) => { + const edge = {}; + + if (workflow.branches[index] === undefined) { + return {}; + } + + var conditions = workflow.branches[index].conditions; + if (conditions === undefined || conditions === null) { + conditions = []; + } + + const label = "Subflow"; + edge.data = { + id: branch.id, + _id: branch.id, + source: branch.source_id, + target: branch.destination_id, + label: label, + decorator: true, + }; + + return edge; + }); + + edges = edges.concat(visualedges); + } + + setWorkflow(workflow); + + // Verifies if a branch is valid and skips others + var newedges = []; + for (var edgeKey in edges) { + var item = edges[edgeKey]; + if (item.data === undefined) { + continue; + } + + const sourcecheck = insertedNodes.find( + (data) => data.data.id === item.data.source + ); + const destcheck = insertedNodes.find( + (data) => data.data.id === item.data.target + ); + if (sourcecheck === undefined || destcheck === undefined) { + continue; + } + + newedges.push(item); + } + + insertedNodes = insertedNodes.concat(newedges); + setElements(insertedNodes); + }; + + const removeNode = (nodeId) => { + const selectedNode = cy.getElementById(nodeId); + // console.log("selected node in removenode", selectedNode); + if (selectedNode.data() === undefined) { + console.log("No node to remove") + return; + } + + //console.log("Removing node: ", selectedNode.data("id"), "Action: ", selectedAction.id) + + // Get selected node + + if (selectedNode.data().type === "TRIGGER") { + console.log("Should remove trigger!"); + console.log(selectedNode.data()); + const triggerindex = workflow.triggers.findIndex( + (data) => data.id === selectedNode.data().id + ); + + setSelectedTriggerIndex(triggerindex); + if (selectedNode.data().trigger_type === "SCHEDULE") { + setSelectedTrigger(selectedNode.data()); + stopSchedule(selectedNode.data(), triggerindex); + } else if (selectedNode.data().trigger_type === "WEBHOOK") { + setSelectedTrigger(selectedNode.data()); + deleteWebhook(selectedNode.data(), triggerindex); + } else if (selectedNode.data().trigger_type === "EMAIL") { + setSelectedTrigger(selectedNode.data()); + stopMailSub(selectedTrigger, triggerindex); + } + } + + //if (selectedNode.data("id") === selectedAction.id) { + // setSelectedApp({}); + // setSelectedAction({}); + //setSelectedTrigger({}); + //setSelectedTriggerIndex({}); + //} + const parsedSelection = cy.$(":selected"); + if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { + alert.info("This node can't be deleted."); + } else { + console.log("Deleted.") + selectedNode.remove(); + + setSelectedTrigger({}) + setSelectedEdge({}) + setSelectedAction({}) + } + + // An attempt at NOT unselecting when removing + /* + setTimeout(() => { + if (parsedSelection.data() !== undefined) { + if (parsedSelection.data("id") !== selectedNode.data("id")) { + console.log("SHOULD SELECT SINCE ID IS DIFFERENT") + + parsedSelection.select() + } + } + + console.log("Parsed: ", parsedSelection.data("id"), selectedNode.data("id")) + }, 2500) + */ + }; + + + if (isLoaded && setupSent === false) { + setSetupSent(true) + + sendStreamRequest({ + "item": "workflow", + "type": "enter", + "userid": userdata.id, + }) + } + + const fetchRecommendations = (inputWorkflow) => { + const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) + + fetch(globalUrl + "/api/v1/workflows/recommend", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(parsedWorkflow), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for usecases"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + console.log("recommendations: ", responseJson); + + if (responseJson.actions !== undefined && responseJson.actions !== null) { + setWorkflowRecommendations(responseJson.actions) + } + } else { + } + }) + .catch((error) => { + //alert.error("ERROR: " + error.toString()); + console.log("ERROR getting usecases: " + error.toString()); + }) + } + + const fetchUsecases = () => { + fetch(globalUrl + "/api/v1/workflows/usecases", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for usecases"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + console.log("Usecases: ", usecases) + setUsecases(responseJson) + } else { + } + }) + .catch((error) => { + //alert.error("ERROR: " + error.toString()); + console.log("ERROR getting usecases: " + error.toString()); + }) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + //useEffect(() => { + if (firstrequest) { + setFirstrequest(false); + getWorkflow(props.match.params.key, {}); + getApps(); + fetchUsecases() + + const cursearch = + typeof window === "undefined" || window.location === undefined + ? "" + : window.location.search; + + // FIXME: Don't check specific one here + const tmpExec = new URLSearchParams(cursearch).get("execution_highlight"); + if ( + tmpExec !== undefined && + tmpExec !== null && + tmpExec === "executions" + ) { + setExecutionModalOpen(true) + const newitem = removeParam("execution_highlight", cursearch); + navigate(curpath + newitem) + //props.history.push(curpath + newitem); + } + + const tmpView = new URLSearchParams(cursearch).get("view"); + if ( + tmpView !== undefined && + tmpView !== null && + tmpView === "executions" + ) { + setExecutionModalOpen(true); + + const newitem = removeParam("view", cursearch); + navigate(curpath + newitem) + //navigate(`?execution_highlight=${parsed_url}`) + //props.history.push(curpath + newitem); + } + return; + } + + // App length necessary cus of cy initialization + if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { + setGraphSetup(true); + setupGraph(); + console.log("In graph setup") + + // 2nd load - configures cytoscape + } else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined) { + console.log("In POST graph setup!") + + + //This part has to load LAST, as it's kind of not async. + //This means we need everything else to happen first. + + setEstablished(true); + // Validate if the node is just a node lol + + console.log("CY grid: ", cy.gridGuide) + + // https://www.npmjs.com/package/cytoscape-grid-guide + // + if (cy.gridGuide !== undefined) { + cy.gridGuide({ + gridSpacing: 30, + guidelinesStyle: { + strokeStyle: "#8b7d6b", // color of geometric guidelines + geometricGuidelineRange: 400, // range of geometric guidelines + range: 100, // max range of distribution guidelines + minDistRange: 10, // min range for distribution guidelines + distGuidelineOffset: 10, // shift amount of distribution guidelines + horizontalDistColor: "#ff0000", // color of horizontal distribution alignment + verticalDistColor: "#00ff00", // color of vertical distribution alignment + initPosAlignmentColor: "#0000ff", // color of alignment to initial mouse location + lineDash: [0, 0], // line style of geometric guidelines + horizontalDistLine: [0, 0], // line style of horizontal distribution guidelines + verticalDistLine: [0, 0], // line style of vertical distribution guidelines + initPosAlignmentLine: [0, 0], // line style of alignment to initial mouse position + } + }) + } else { + console.log("ERROR: Failed to render grid as it's unitialized") + } + + if (cy.edgehandles !== undefined) { + cy.edgehandles({ + handleNodes: (el) => { + if (el.isNode() && + !el.data("isButton") && + !el.data("isDescriptor") && + !el.data("isSuggestion") && + el.data("type") !== "COMMENT") { + return true + } + + return false + }, + preview: false, + toggleOffOnLeave: true, + loopAllowed: function (node) { + return false; + }, + }); + + //cy.edgehandles({ + // preview: false, + //}) + + //cy.edgehandles().enable() + } else { + console.log("ERROR: Failed to initialize edgehandler") + } + // preview: true, + + cy.fit(null, 200); + + cy.on("boxselect", "node", (e) => { + if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) { + e.target.unselect(); + } + + e.target.addClass("selected"); + }); + + cy.on("boxstart", (e) => { + console.log("START"); + cy.removeListener("select"); + }); + + cy.on("boxend", (e) => { + console.log("END: ", cy) + var cydata = cy.$(":selected").jsons(); + if (cydata !== undefined && cydata !== null && cydata.length > 0) { + alert.success(`Selected ${cydata.length} element(s). CTRL+C to copy them.`); + } + }); + + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + + cy.on("unselect", (e) => onUnselect(e)); + + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + + cy.on("cxttap", "node", (e) => onCtxTap(e)); + + document.title = "Workflow - " + workflow.name; + registerKeys(); + } + //}) + + const stopSchedule = (trigger, triggerindex) => { + fetch( + `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule/${trigger.id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + // No matter what, it's being stopped. + if (!responseJson.success) { + if (responseJson.reason !== undefined) { + alert.error("Failed to stop schedule: " + responseJson.reason); + } + } else { + alert.success("Successfully stopped schedule"); + } + + workflow.triggers[triggerindex].status = "stopped"; + trigger.status = "stopped"; + setSelectedTrigger(trigger); + setWorkflow(workflow); + saveWorkflow(workflow); + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Stop schedule error: ", error.toString()); + }); + }; + + const submitSchedule = (trigger, triggerindex) => { + if (trigger.name.length <= 0) { + alert.error("Error: name can't be empty"); + return; + } + + var mappedStartnode = "" + const alledges = cy.edges().jsons() + if (alledges !== undefined && alledges !== null && alledges.length > 0) { + for (let edgekey in alledges) { + const tmp = alledges[edgekey] + console.log("TMP: ", tmp, tmp.data.source) + if (tmp.data.source === trigger.id) { + mappedStartnode = tmp.data.target + break } - newAppname = newAppname.replaceAll("_", " ") - - //const image = "url("+app.large_image+")" - const image = app.large_image - const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) - const pixelSize = !hover ? "2px" : "4px" - newAppStyle.borderLeft = app.is_valid ? `${pixelSize} solid ${green}` : `${pixelSize} solid ${yellow}` - - return ( - {handleAppDrag(e, app)}} - onStop={(e) => { - handleDragStop(e, app) - }} - key={app.id} - dragging={false} - position={{ - x: 0, - y: 0, - }} - > - {setHover(true)}} onMouseOut={() => {setHover(false)}}> - - - {newAppname} - - - - - {newAppname} - - - - - Version: {app.app_version} - - - - - {app.description} - - - - - - - ) } + } - const runSearch = (value) => { - if (value.length > 0) { - var newApps = allApps.filter(app => (app.name.toLowerCase().includes(value.trim().toLowerCase() || app.description.toLowerCase().includes(value.trim().toLowerCase()))) && !(!app.activated && app.generated)) + alert.info("Creating schedule") + const data = { + name: trigger.name, + frequency: workflow.triggers[triggerindex].parameters[0].value, + execution_argument: workflow.triggers[triggerindex].parameters[1].value, + environment: workflow.triggers[triggerindex].environment, + id: trigger.id, + start: mappedStartnode, + } - // Extend search - if (newApps.length === 0) { - const searchvalue = value.trim().toLowerCase() - newApps = allApps.filter(app => { - for (var key in app.actions) { - const inneraction = app.actions[key] - if (inneraction.name.toLowerCase().includes(searchvalue)) { - return true - } - } + fetch( + `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } - return false - }) + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + alert.error("Failed to set schedule: " + responseJson.reason); + } else { + alert.success("Successfully created schedule"); + workflow.triggers[triggerindex].status = "running"; + trigger.status = "running"; + setSelectedTrigger(trigger); + setWorkflow(workflow); + console.log("Should set the status to running and save"); + saveWorkflow(workflow); + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get schedule error: ", error.toString()); + }); + }; + + const appViewStyle = { + marginLeft: 5, + marginRight: 5, + display: "flex", + flexDirection: "column", + minHeight: isMobile ? bodyHeight - appBarSize * 4 : "100%", + maxHeight: isMobile ? bodyHeight - appBarSize * 4 : "100%", + }; + + const paperAppStyle = { + borderRadius: theme.palette.borderRadius, + minHeight: isMobile ? 50 : 100, + maxHeight: isMobile ? 50 : 100, + minWidth: isMobile ? 50 : "100%", + maxWidth: isMobile ? 50 : "100%", + marginTop: "5px", + color: "white", + backgroundColor: surfaceColor, + cursor: "pointer", + display: "flex", + }; + + const paperVariableStyle = { + borderRadius: theme.palette.borderRadius, + minHeight: 50, + maxHeight: 50, + minWidth: "100%", + maxWidth: "100%", + marginTop: "5px", + color: "white", + backgroundColor: surfaceColor, + cursor: "pointer", + display: "flex", + }; + + const VariableItem = (props) => { + const { variable, index, type } = props; + + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + + const menuClick = (event) => { + setOpen(!open); + setAnchorEl(event.currentTarget); + }; + + const deleteVariable = (type, variableIndex) => { + + console.log("Delete type: ", type, variableIndex) + + if (type === "normal") { + if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) + vars.splice(variableIndex, 1) + workflow.workflow_variables = vars + + console.log("Workflow after del: ", workflow) + + setWorkflow(workflow); + setUpdate(Math.random()); } + } else if (type === "exec") { + if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) + vars.splice(variableIndex, 1) + workflow.execution_variables = vars - //setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated))) - //console.log("FOUND: ", newApps) - setVisibleApps(newApps) - } else { - setVisibleApps(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)))) + console.log("Workflow after del: ", workflow) + + setWorkflow(workflow); + setUpdate(Math.random()); + } } - } + }; - return( -
    -
    - - - - - - ) - }} - fullWidth - color="primary" - placeholder={"Search Active Apps"} - id="appsearch" - onKeyPress={(event) => { - if (event.key === "Enter") { - event.target.blur(event) - } - }} - onBlur={(event) => { - console.log("BLUR: ", event.target.value) - runSearch(event.target.value) + return ( +
    + { }}> +
    - {visibleApps.length > 0 ? -
    - {visibleApps.map((app, index) => { - if (app.invalid) { - return null - } - - return( - - ) - })} -
    - : - apps.length > 0 ? -
    - - Couldn't find app. Is it active? - -
    - : -
    - - - Loading apps - -
    - } -
    -
    - ) - } - - const getNextActionName = (appName) => { - var highest = "" - //label = name + _number - const allitems = workflow.actions.concat(workflow.triggers) - for (var key in allitems) { - const item = allitems[key] - if (item.app_name === appName && item.label !== undefined && item.label !== null) { - var number = item.label.split("_") - if (isNaN(number[-1]) && parseInt(number[number.length-1]) > highest) { - highest = number[number.length-1] - } - } - } - - if (highest) { - return appName+"_"+(parseInt(highest)+1) - } else { - return appName+"_"+1 - } - } - - const setNewSelectedAction = (e) => { - const newaction = selectedApp.actions.find(a => a.name === e.target.value) - if (newaction === undefined || newaction === null) { - alert.error("Failed to find the action") - return - } - - // Does this one find the wrong one? - var newSelectedAction = selectedAction - newSelectedAction.name = newaction.name - newSelectedAction.parameters = JSON.parse(JSON.stringify(newaction.parameters)) - newSelectedAction.errors = [] - newSelectedAction.isValid = true - newSelectedAction.is_valid = true - - if (newSelectedAction.app_name === "Shuffle Tools") { - const iconInfo = GetIconInfo(newSelectedAction) - console.log("ICONINFO: ", iconInfo) - const svg_pin = `` - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) - newSelectedAction.large_image = svgpin_Url - newSelectedAction.fillGradient = iconInfo.fillGradient - newSelectedAction.fillstyle = "solid" - if (newSelectedAction.fillGradient !== undefined && newSelectedAction.fillGradient !== null && newSelectedAction.fillGradient.length > 0) { - newSelectedAction.fillstyle = 'linear-gradient' - console.log("GRADIENT!: ", newSelectedAction) - //action.fillstyle = - //'background-fill': 'data(fillstyle)', - } else { - newSelectedAction.iconBackground = iconInfo.iconBackgroundColor - } - - const foundnode = cy.getElementById(newSelectedAction.id) - if (foundnode !== null && foundnode !== undefined) { - console.log("UPDATING NODE!") - foundnode.data(newSelectedAction) - } - } - - // Takes an action as input, then runs through and updates the relevant fields - // based on previous actions' - newSelectedAction = RunAutocompleter(newSelectedAction) - - console.log("ACTION: ", newSelectedAction) - - if (newaction.returns.example !== undefined && newaction.returns.example !== null && newaction.returns.example.length > 0) { - newSelectedAction.example = newaction.returns.example - } - - // FIXME - this is broken sometimes lol - //var env = environments.find(a => a.Name === newaction.environment) - //if ((!env || env === undefined) && selectedAction.environment === undefined ) { - // env = environments[defaultEnvironmentIndex] - //} - //setSelectedActionEnvironment(env) - - setSelectedAction(newSelectedAction) - setUpdate(Math.random()) - - // FIXME - should change icon-node (descriptor) as well - const allNodes = cy.nodes().jsons() - for (var key in allNodes) { - const currentNode = allNodes[key] - if (currentNode.data.attachedTo === selectedAction.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 - } - } - } - - // APPSELECT at top - // appname & version - // description - // ACTION select - // - const selectedNameChange = (event) => { - //console.log("OLDNAME: ", selectedAction.name) - 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 - setSelectedAction(selectedAction) - - //console.log("SHOULD CHANGE NAME EVERYWHERE ITS USED TOO BASED ON OLD NAME!") - - /* - if (nodeaction.label !== curaction.label) { - console.log("BEACH!") - - var params = [] - const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_") - for (var actionkey in workflow.actions) { - if (workflow.actions[actionkey].id === curaction.id) { - continue - } - - for (var paramkey in workflow.actions[actionkey].parameters) { - const param = workflow.actions[actionkey].parameters[paramkey] - if (param.value === null || param.value === undefined || !param.value.includes("$")) { - continue - } - - const innername = param.value.toLowerCase().replace(" ", "_") - if (innername.includes(fixedName)) { - //workflow.actions[actionkey].parameters[paramkey].replace( - //console.log("FOUND!: ", innername) - } - } - } - } - */ - } - - const selectedTriggerChange = (event) => { - selectedTrigger.label = event.target.value - setSelectedTrigger(selectedTrigger) - } - - // Starts on current node and climbs UP the tree to the root object. - // Sends back everything in it's path - const getParents = (action) => { - var allkeys = [action.id] - var handled = [] - var results = [] - - // maxiter = max amount of parent nodes to loop - // also handles breaks if there are issues - var iterations = 0 - var maxiter = 10 - while(true) { - for (var key in allkeys) { - var currentnode = cy.getElementById(allkeys[key]) - if (currentnode === undefined || currentnode === null) { - continue - } - - if (currentnode.data() === undefined) { - //console.log("Node doesn't have any data (getParents)! Probably a trigger.") - handled.push(allkeys[key]) - results.push({"id": allkeys[key], "type": "TRIGGER"}) - } else { - if (handled.includes(currentnode.data().id)) { - continue - } else { - handled.push(currentnode.data().id) - results.push(currentnode.data()) - } - } - - // Get the name / label here too? - if (currentnode.length === 0) { - continue - } - - const incomingEdges = currentnode.incomers('edge') - if (incomingEdges.length === 0) { - continue - } - - for (var i = 0; i < incomingEdges.length; i++) { - var tmp = incomingEdges[i] - if (!allkeys.includes(tmp.data().source)) { - allkeys.push(tmp.data().source) - } - } - } - - if (results.length === allkeys.length || iterations === maxiter) { - break - } - - iterations += 1 - } - - // Remove self - results = results.filter(data => data.id !== action.id) - results = results.filter(data => data.type !== "TRIGGER") - results.push({"label": "Execution Argument", "type": "INTERNAL"}) - return results - } - - // BOLD name: type: required? - // FORM - // Dropdown -> static, action, local env, global env - // VALUE (JSON) - // {data.name}, {data.description}, {data.required}, {data.schema.type} - - //height: "100%", - const appApiViewStyle = { - display: "flex", - flexDirection: "column", - backgroundColor: "#1F2023", - color: "white", - paddingRight: 15, - paddingLeft: 15, - minHeight: "100%", - zIndex: 1000, - resize: "vertical", - overflow: "auto", - } - - // Old static one - //var rightsidebarStyle = { - // position: "fixed", - // right: 0, - // top: appBarSize+1, - // height: "100%", - // bottom: 0, - // minWidth: 350, - // maxWidth: 350, - // borderLeft: "1px solid rgb(91, 96, 100)", - // overflow: "scroll", - // overflowX: "auto", - // overflowY: "auto", - // zIndex: 1000, - //} - - var rightsidebarStyle = { - position: "fixed", - top: appBarSize+25, - right: 25, - height: "80vh", - width: 350, - minWidth: 200, - maxWidth: 600, - maxHeight: "100vh", - border: "1px solid rgb(91, 96, 100)", - zIndex: 1000, - borderRadius: theme.palette.borderRadius, - resize: "both", - overflow: "auto", - } - - const setTriggerFolderWrapperMulti = event => { - const { options } = event.target - const value = [] - for (let i = 0, l = options.length; i < l; i += 1) { - if (options[i].selected) { - value.push(options[i].value) - } - } - - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = [[]] - workflow.triggers[selectedTriggerIndex].parameters = [[]] - } - - // This is a dirty workaround for the static values in the go backend and datastore db - const fixedValue = value.join(splitter) - selectedTrigger.parameters[0] = {"value": fixedValue, "name": "outlookfolder"} - workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": fixedValue, "name": "outlookfolder"} - - // This resets state for some reason (: - setSelectedAction({}) - setSelectedTrigger({}) - setSelectedApp({}) - setSelectedEdge({}) - - // Set value - setSelectedTrigger(selectedTrigger) - setWorkflow(workflow) - }; - - //const setTriggerFolderWrapper = (event) => { - // if (selectedTrigger.parameters === null) { - // selectedTrigger.parameters = [] - // workflow.triggers[selectedTriggerIndex].parameters = [] - // } - - // const folder = triggerFolders.find(a => a.displayName === event.target.value) - // console.log(event.target.value) - // console.log(folder) - // - // if (folder !== undefined) { - // workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id} - // selectedTrigger.parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id} - // setWorkflow(workflow) - - // // This resets state for some reason (: - // setSelectedActionEnvironment({}) - // setSelectedAction({}) - // setSelectedTrigger({}) - // setSelectedApp({}) - // setSelectedEdge({}) - - // // Set value - // setSelectedTrigger(selectedTrigger) - - // } else { - // alert.error("Some error occurred with folder "+event.target.value) - // } - //} - - const setTriggerCronWrapper = (value) => { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = [] - } - - workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"} - setWorkflow(workflow) - } - - const setTriggerOptionsWrapper = (value) => { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = [] - } - - const splitItems = workflow.triggers[selectedTriggerIndex].parameters[2].value.split(",") - console.log(splitItems) - if (splitItems.includes(value)) { - for( var i = 0; i < splitItems.length; i++){ - if (splitItems[i] === value) { - splitItems.splice(i, 1) - } - } - - } else { - splitItems.push(value) - } - - for( var i = 0; i < splitItems.length; i++){ - if (splitItems[i] === "") { - splitItems.splice(i, 1) - } - } - - workflow.triggers[selectedTriggerIndex].parameters[2].value = splitItems.join(",") - - console.log(splitItems) - setWorkflow(workflow) - setLocalFirstrequest(!localFirstrequest) - } - - const setTriggerTextInformationWrapper = (value) => { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = [] - } - - workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "alertinfo"} - setWorkflow(workflow) - } - - const setTriggerBodyWrapper = (value) => { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = [] - workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"} - } - - workflow.triggers[selectedTriggerIndex].parameters[1] = {"value": value, "name": "execution_argument"} - setWorkflow(workflow) - } - - const AppConditionHandler = (props) => { - const { tmpdata, type } = props - const [data, ] = useState(tmpdata) - const [multiline, setMultiline] = useState(false) - const [showAutocomplete, setShowAutocomplete] = React.useState(false) - const [actionlist, setActionlist] = React.useState([]) - - if (tmpdata === undefined) { - return tmpdata - } - - if (data.variant === "") { - data.variant = "STATIC_VALUE" - } - - - // Set actions based on NEXT node, since it should be able to involve those two - //console.log("IN APPACTIONARG: ", selectedEdge) - if (actionlist.length === 0) { - // FIXME: Have previous execution values in here - actionlist.push({"type": "Execution Argument", "name": "Execution Argument", "value": "$exec", "highlight": "exec", "autocomplete": "exec", "example": "hello"}) - //actionlist.push({"type": "Key:Value store", "name": "Shuffle KV store", "value": "$shuffle_cache", "highlight": "shuffle_cache", "autocomplete": "shuffle_cache", "example": ""}) - - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - for (var key in 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}) - } - } - - // FIXME: Add values from previous executions if they exist - if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) { - for (var key in workflow.execution_variables) { - const item = workflow.execution_variables[key] - actionlist.push({"type": "execution_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": ""}) - } - } - - const destAction = cy.getElementById(selectedEdge.target) - //console.log(destAction.data()) - var parents = getParents(destAction.data()) - if (parents.length > 1) { - for (var key in parents) { - const item = parents[key] - if (item.label === "Execution Argument") { - continue - } - - // 1. Take - const actionvalue = {"type": "action", "id": item.id, "name": item.label, "autocomplete": `${item.label.split(" ").join("_")}`, "example": item.example === undefined ? "" : item.example} - actionlist.push(actionvalue) - } - } - - setActionlist(actionlist) - } - - var staticcolor = "inherit" - var actioncolor = "inherit" - var varcolor = "inherit" - if (data.multiline !== undefined && data.multiline !== null && data.multiline === true) { - setMultiline(true) - } - - var placeholder = "Static value" - if (data.example !== undefined && data.example !== null && data.example.length > 0) { - placeholder = data.example - } - - var datafield = - - Use "Shuffle Tools" app with "Filter List" action to handle loops - - : null - } - onClick={() => { - //console.log("CHANGE FIELD") - }} - onBlur={(e) => { - changeActionVariable(data.action_field, e.target.value) - setUpdate(Math.random()) - }} - /> - - const changeActionVariable = (variable, value) => { - // set the name - data.value = value - data.action_field = variable - - //setConditionValue({}) - - if (type === "source") { - setSourceValue(data) - //setDestinationValue(destinationValue) - } else if (type === "destination") { - setDestinationValue(data) - //setSourceValue(sourceValue) - } - } - - const changeActionParameterVariant = (variant) => { - if (data.variant === variant) { - return - } - - data.variant = variant - data.value = "" - - if (variant === "ACTION_RESULT") { - console.log("SHOULD FIND PARENTS OF EDGE") - - // Uses the target's parents, as the target should be executing the checks (I think) - var parents = getParents(workflow.actions.find(a => a.id === selectedEdge["target"])) - if (parents.length > 0) { - data.action_field = parents[0].label - } else { - data.action_field = "" - } - } else if (variant === "WORKFLOW_VARIABLE") { - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - data.action_field = workflow.workflow_variables[0].name - } - } - - setSourceValue({}) - setConditionValue({}) - setDestinationValue({}) - - if (type === "source") { - setSourceValue(data) - setDestinationValue(destinationValue) - } else if (type === "destination") { - setDestinationValue(data) - setSourceValue(sourceValue) - } - } - - return ( -
    -
    -
    -
    - {data.name} -
    -
    - {datafield} - {actionlist.length === 0 ? null : - - Autocomplete - - - } -
    - ) - } - - - const menuItemStyle = { - color: "white", - backgroundColor: inputColor - } - - const conditionsModal = - { - setConditionsModalOpen(false) - setSourceValue({}) - setConditionValue({}) - setDestinationValue({}) - }} - > - - Conditions can't be used for loops [ .# ] Learn more - - - Condition - - - -
    - - - - - -
    - -
    -
    - - { - setVariableAnchorEl(null) - }} - > - { - conditionValue.value = "equals" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"equals"}>equals - { - conditionValue.value = "does not equal" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"does not equal"}>does not equal - { - conditionValue.value = "startswith" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"starts with"}>starts with - { - conditionValue.value = "endswith" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"ends with"}>ends with - { - conditionValue.value = "contains" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"contains"}>contains - { - conditionValue.value = "contains_any_of" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"contains_any_of"}>contains any of - { - conditionValue.value = "matches regex" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"matches regex"}>matches regex - { - conditionValue.value = "larger than" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"larger than"}>larger than - { - conditionValue.value = "less than" - setConditionValue(conditionValue) - setVariableAnchorEl(null) - }} key={"less than"}>less than - -
    -
    - -
    - -
    -
    - - - -
    -
    - - const EdgeSidebar = () => { - const ConditionHandler = (condition, index) => { - const [open, setOpen] = React.useState(false) - const [anchorEl, setAnchorEl] = React.useState(null) - - const duplicateCondition = (conditionIndex) => { - - var newEdge = JSON.parse(JSON.stringify(selectedEdge.conditions[conditionIndex])) - const newUuid = uuidv4() - newEdge.condition.id = newUuid - newEdge.source.id = newUuid - newEdge.destination.id = newUuid - selectedEdge.conditions.push(newEdge) - - setUpdate(Math.random()) - } - - const deleteCondition = (conditionIndex) => { - console.log(selectedEdge) - if (selectedEdge.conditions.length === 1) { - selectedEdge.conditions = [] - } else { - selectedEdge.conditions.splice(conditionIndex, 1) - } - - setSelectedEdge(selectedEdge) - setOpen(false) - setUpdate(Math.random()) - } - - const paperVariableStyle = { - minHeight: 75, - maxHeight: 75, - minWidth: "100%", - maxWidth: "100%", - marginTop: "5px", - color: "white", - backgroundColor: surfaceColor, - cursor: "pointer", - display: "flex", - } - - const menuClick = (event) => { - console.log("MENU CLICK") - setOpen(!open) - setAnchorEl(event.currentTarget) - } - - return ( - {}}> -
    -
    -
    { - setSourceValue(condition.source) - setConditionValue(condition.condition) - setDestinationValue(condition.destination) - setConditionsModalOpen(true) - }}> -
    - {condition.source.value} -
    - -
    {}}> - {condition.condition.value} -
    - -
    - {condition.destination.value} -
    + }} + > + Name: {variable.name}
    -
    +
    + style={{ color: "white" }} + > { - setOpen(false) - setAnchorEl(null) - }} + id="long-menu" + anchorEl={anchorEl} + keepMounted + open={open} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + }, + }} + onClose={() => { + setOpen(false); + setAnchorEl(null); + }} > - { - duplicateCondition(index) - }} key={"Duplicate"}>{"Duplicate"} - { - setOpen(false) - deleteCondition(index) - }} key={"Delete"}>{"Delete"} + { + setOpen(false); + setVariableInfo({ + "name": variable.name, + "description": variable.description, + "value": variable.value, + "index": index, + }) + + if (type === "normal") { + setVariablesModalOpen(true); + } else if (type === "exec") { + setExecutionVariablesModalOpen(true); + } else { + console.log("Unknown type: ", type) + } + }} + key={"Edit"} + > + {"Edit"} + + { + deleteVariable(type, index); + setOpen(false); + }} + key={"Delete"} + > + {"Delete"} +
    - ) - } - - var injectedData = -
    - - if (selectedEdge.conditions !== undefined && selectedEdge.conditions !== null && selectedEdge.conditions.length > 0) { - injectedData = selectedEdge.conditions.map((condition, index) => { - return ConditionHandler(condition, index) - }) - } - - // FIXME - remove index - const conditionId = uuidv4() - return( -
    -
    -
    -

    Branch: Conditions - {selectedEdgeIndex}

    - What are conditions? -
    -
    - -
    - Conditions -
    - {injectedData} - - -
    ) } - // 1. GET the trigger authentication data - // 2. Parse the fields that are used (outlook & gmail) - // 3. Parse the folders that are selected - // 4. Start / stop - const EmailSidebar = () => { - if (Object.getOwnPropertyNames(selectedTrigger).length === 0) { - return null - } + const VariablesView = () => { + const variableScrollStyle = { + margin: 15, + overflow: "scroll", + height: isMobile ? "100%" : "66vh", + overflowX: "auto", + overflowY: "auto", + flex: "10", + }; - if (workflow.triggers[selectedTriggerIndex] === undefined) { - return null - } + return ( +
    +
    + What are{" "} + + Workflow variables? + + {workflow.workflow_variables === null + ? null + : workflow.workflow_variables.map((variable, varindex) => { + return ( + + ); + })} - if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) { - workflow.triggers[selectedTriggerIndex].parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}] - selectedTrigger.parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}] - setWorkflow(workflow) - setSelectedTrigger(selectedTrigger) - } +
    + - - const outlookButton = selectedTrigger.name !== "Office365" ? null : - - - - // FIXME - set everything in here to multifolder etc - var triggerInfo = "SET UP BUT NO TYPE :)" - if (Object.getOwnPropertyNames(triggerAuthentication).length > 0) { - // Should get the folders if they don't already exist - - if (triggerAuthentication.type === "outlook" || triggerAuthentication.type === "gmail" ) { - triggerInfo =
    - {selectedTrigger.status === "running" ? null : - -
    -
    -
    - Change auth -
    -
    - {outlookButton} - {gmailButton} - - } - - {triggerFolders === undefined || triggerFolders === null ? - null : - -
    -
    -
    - Select folders -
    -
    - } - key={selectedTrigger} - > - {triggerFolders.map(folder => { - //console.log("FOLDER: ", folder) - //var folderItem = - - if (folder.childFolderCount > 0) { - // Here to handle subfolders sometime later - folderItem = - - } - - return folderItem - })} - - - } -
    - } else if (triggerAuthentication.type === "gmail") { - console.log("AUTH: ", triggerAuthentication) - triggerInfo = "SPECIAL GMAIL" - } - } - - - // Check - const argumentView = Object.getOwnPropertyNames(triggerAuthentication).length > 0 ? -
    - {triggerInfo} -
    - : -
    -
    -
    -
    - Login -
    -
    - {outlookButton} - {gmailButton} -
    - - return( -
    -
    -
    -

    {selectedTrigger.app_name}: {selectedTrigger.status}

    - What are email triggers? -
    -
    - -
    - Name -
    - - -
    - Environment: - -
    - - {argumentView} -
    -
    - -
    - - -
    -
    -
    -
    - ) - } - - const SubflowSidebar = () => { - const [menuPosition, setMenuPosition] = useState(null) - const [showDropdown, setShowDropdown] = React.useState(false) - const [showDropdownNumber, setShowDropdownNumber] = React.useState(0) - const [showAutocomplete, setShowAutocomplete] = React.useState(false) - const [actionlist, setActionlist] = React.useState([]) - - if (actionlist.length === 0) { - // FIXME: Have previous execution values in here - actionlist.push({"type": "Execution Argument", "name": "Execution Argument", "value": "$exec", "highlight": "exec", "autocomplete": "exec", "example": "hello"}) - if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { - for (var key in 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}) - } - } - - // FIXME: Add values from previous executions if they exist - if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) { - for (var key in workflow.execution_variables) { - const item = workflow.execution_variables[key] - actionlist.push({"type": "execution_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": ""}) - } - } - - var parents = getParents(selectedAction) - if (parents.length > 1) { - for (var key in parents) { - const item = parents[key] - if (item.label === "Execution Argument") { - continue - } - - var exampledata = item.example === undefined ? "" : item.example - // Find previous execution and their variables - //exampledata === "" && - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false - for (var key in 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) { - continue - } - - foundResult.result = foundResult.result.trim() - foundResult.result = foundResult.result.split(" None").join(" \"None\"") - foundResult.result = foundResult.result.split(" False").join(" false") - foundResult.result = foundResult.result.split(" True").join(" true") - - var jsonvalid = true - try { - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false - } - } catch (e) { - try { - foundResult.result = foundResult.result.split("\'").join("\"") - const tmp = String(JSON.parse(foundResult.result)) - if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - } - - // Finds the FIRST json only - if (jsonvalid) { - exampledata = JSON.parse(foundResult.result) - break - } - //else { - // console.log("Invalid JSON: ", foundResult.result) - //} - } - } - - // 1. Take - const actionvalue = {"type": "action", "id": item.id, "name": item.label, "autocomplete": `${item.label.split(" ").join("_")}`, "example": exampledata} - actionlist.push(actionvalue) - } - - } - - //console.log("ACTIONS: ", actionlist) - setActionlist(actionlist) - } - - // Shows nested list of nodes > their JSON lists - const ActionlistWrapper = (props) => { - const { data } = props; - - const handleMenuClose = () => { - setShowAutocomplete(false) - - //if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === "$") { - // setShowDropdown(false) - //} - - setUpdate(Math.random()) - setMenuPosition(null) - } - - const handleItemClick = (values) => { - if (values === undefined || values === null || values.length === 0) { - return - } - - var toComplete = workflow.triggers[selectedTriggerIndex].parameters[1].value.trim().endsWith("$") ? values[0].autocomplete : "$"+values[0].autocomplete - for (var key in values) { - if (key == 0 || values[key].autocomplete.length === 0) { - continue - } - - toComplete += values[key].autocomplete - } - - // 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) { - paramcheck["value_replace"] = [{ - "key": data.name, - "value": toComplete, - }] - - } else { - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - "key": data.name, - "value": toComplete, + setVariableInfo({ + "name": "", + "description": "", + "value": "", }) - } else { - paramcheck["value_replace"][subparamindex]["value"] += toComplete + }} + > + New workflow variable + +
    + + What are{" "} + + Runtime variables? + + {workflow.execution_variables === null || + workflow.execution_variables === undefined + ? null + : workflow.execution_variables.map((variable, varindex2) => { + return ( + + ); + })} +
    + +
    +
    +
    + ); + }; + + const handleSetTab = (event, newValue) => { + setCurrentView(newValue); + }; + + const HandleLeftView = () => { + // Defaults to apps. + var thisview = ( + + ); + if (currentView === 1) { + thisview = ; + } else if (currentView === 2) { + thisview = ; + } + + const tabStyle = { + maxWidth: isMobile ? leftBarSize : leftBarSize / 3, + minWidth: isMobile ? leftBarSize : leftBarSize / 3, + flex: 1, + textTransform: "none", + }; + + const iconStyle = { + marginTop: 3, + marginRight: 5, + }; + + const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 50 + return ( +
    +
    + {thisview} +
    + + + + + + + {isMobile ? null : Apps} + + } + style={tabStyle} + /> + + + + + {isMobile ? null : Triggers} + + } + style={tabStyle} + /> + + + + + {isMobile ? null : Variables} + + } + style={tabStyle} + /> + +
    + ); + }; + + + + const TriggersView = () => { + const triggersViewStyle = { + marginLeft: "10px", + marginRight: "10px", + display: "flex", + flexDirection: "column", + }; + + // Predefined hurr + + return ( +
    +
    + {triggers.map((trigger, index) => { + var imageline = + trigger.large_image.length === 0 ? ( + + ) : ( + + ); + + const title = trigger.trigger_type === "WEBHOOK" ? "Workflow starters" : trigger.trigger_type === "SUBFLOW" ? "Mid-Workflow" : "" + + const color = trigger.is_valid ? green : yellow; + return ( + + {title.length > 0 ? + + {title} + + : null} + + { + handleTriggerDrag(e, trigger); + }} + onStop={(e) => { + handleDragStop(e); + }} + dragging={false} + position={{ + x: 0, + y: 0, + }} + > + { }}> +
    + + + {imageline} + + {isMobile ? null : + + +

    + {trigger.name} +

    +
    + + {trigger.description} + +
    + } +
    +
    +
    +
    + ); + })} +
    +
    + ); + }; + + var newNodeId = ""; + var parsedApp = {}; + const handleTriggerDrag = (e, data) => { + const cycontainer = cy.container(); + // Chrome lol + if ( + e.pageX > cycontainer.offsetLeft && + e.pageX < cycontainer.offsetLeft + cycontainer.offsetWidth && + e.pageY > cycontainer.offsetTop && + e.pageY < cycontainer.offsetTop + cycontainer.offsetHeight + ) { + if (newNodeId.length > 0) { + var currentnode = cy.getElementById(newNodeId); + if (currentnode.length === 0) { + return; + } + + currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft); + currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop); + } else { + if (workflow.start === "" || workflow.start === undefined) { + alert.error("Define a starting action first."); + return; + } + + const triggerLabel = getNextActionName(data.name); + + newNodeId = uuidv4(); + const newposition = { + x: e.pageX - cycontainer.offsetLeft, + y: e.pageY - cycontainer.offsetTop, + }; + + const newAppData = { + app_name: data.name, + app_version: "1.0.0", + environment: isCloud ? "cloud" : data.environment, + description: data.description, + long_description: data.long_description, + errors: [], + id_: newNodeId, + _id_: newNodeId, + id: newNodeId, + finished: false, + label: triggerLabel, + type: data.type, + is_valid: true, + trigger_type: data.trigger_type, + large_image: data.large_image, + status: "uninitialized", + name: data.name, + isStartNode: false, + position: newposition, + }; + + // Can all the data be in here? hmm + const nodeToBeAdded = { + group: "nodes", + data: newAppData, + renderedPosition: newposition, + }; + + cy.add(nodeToBeAdded); + parsedApp = nodeToBeAdded; + return; + } + } + }; + + const handleDragStop = (e, app) => { + var currentnode = cy.getElementById(newNodeId); + if ( + currentnode === undefined || + currentnode === null || + currentnode.length === 0 + ) { + return; + } + + // Using remove & replace, as this triggers the function + // onNodeAdded() with this node after it's added + + currentnode.remove(); + parsedApp.data.finished = true; + parsedApp.data.position = currentnode.renderedPosition(); + parsedApp.position = currentnode.renderedPosition(); + parsedApp.renderedPosition = currentnode.renderedPosition(); + + var newAppData = parsedApp.data; + if (newAppData.type === "ACTION") { + + //const activateApp = (appid) => { + if (newAppData.activated === false) { + activateApp(newAppData.app_id, false) + } + + // AUTHENTICATION + if (app.authentication !== undefined && app.authentication !== null && app.authentication.required === true) { + + // Setup auth here :) + const authenticationOptions = []; + var findAuthId = ""; + if ( + newAppData.authentication_id !== null && + newAppData.authentication_id !== undefined && + newAppData.authentication_id.length > 0 + ) { + findAuthId = newAppData.authentication_id; + } + + const tmpAuth = JSON.parse(JSON.stringify(appAuthentication)); + for (let authkey in tmpAuth) { + if (authkey === undefined) { + continue + } + + var item = tmpAuth[authkey]; + const newfields = {}; + for (let fieldkey in item.fields) { + if (item.fields[fieldkey] === undefined) { + console.log("Problem with filterkey in Node select", fieldkey) + continue + } + + const filterkey = item.fields[fieldkey]["key"] + if (filterkey === null || filterkey === undefined) { + console.log("Problem with filterkey 2. Null or undefined 3") + continue + } + + newfields[filterkey] = item.fields[fieldkey]["value"]; + } + + item.fields = newfields; + if (item.app.id === app.id || item.app.name === app.name) { + authenticationOptions.push(item); + + if (item.id === findAuthId) { + newAppData.selectedAuthentication = item + newAppData.authentication_id = item.id + + } else if (findAuthId === "") { + // Will always be set to the last one if one isn't found. + // Last = timestamp too + newAppData.selectedAuthentication = item + newAppData.authentication_id = item.id + } + } + } + + if ( + authenticationOptions !== undefined && + authenticationOptions !== null && + authenticationOptions.length > 0 + ) { + for (let authkey in authenticationOptions) { + const option = authenticationOptions[authkey]; + + if (option.active && newAppData.authentication_id === "") { + newAppData.selectedAuthentication = option; + newAppData.authentication_id = option.id; + break; + } + } + } + } else { + newAppData.authentication = []; + newAppData.authentication_id = ""; + newAppData.selectedAuthentication = {}; + } + + parsedApp.data = newAppData; + cy.add(parsedApp); + } else if (newAppData.type === "TRIGGER") { + cy.add(parsedApp); + } + + newNodeId = ""; + parsedApp = {}; + }; + + const appScrollStyle = { + overflow: "scroll", + maxHeight: isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 55 - 50, + minHeight: isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 55 - 50, + marginTop: 1, + overflowY: "auto", + overflowX: "hidden", + }; + + const handleAppDrag = (e, app) => { + const cycontainer = cy.container(); + + //console.log("e: ", e) + //console.log("Offset: ", cycontainer) + + // Chrome lol + if ( + e.pageX > cycontainer.offsetLeft && + e.pageX < cycontainer.offsetLeft + cycontainer.offsetWidth && + e.pageY > cycontainer.offsetTop && + e.pageY < cycontainer.offsetTop + cycontainer.offsetHeight + ) { + if (newNodeId.length > 0) { + var currentnode = cy.getElementById(newNodeId); + if ( + currentnode === undefined || + currentnode === null || + currentnode.length === 0 + ) { + return; + } + + currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft); + currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop); + } else { + if (workflow.public) { + console.log("workflow is public - not adding"); + return; + } + + if ( + app.actions === undefined || + app.actions === null || + app.actions.length === 0 + ) { + alert.error( + "App " + + app.name + + " currently has no actions to perform. Please go to https://shuffler.io/apps to edit it." + ); + return; + } + + newNodeId = uuidv4(); + const actionType = "ACTION"; + const actionLabel = getNextActionName(app.name); + console.log("Next action name: ", actionLabel) + var parameters = null; + var example = ""; + var description = "" + + if ( + app.actions[0].parameters !== undefined && + app.actions[0].parameters !== null && + app.actions[0].parameters.length > 0 + ) { + parameters = app.actions[0].parameters; + } + + if ( + app.actions[0].returns !== undefined && + app.actions[0].returns !== null && + app.actions[0].returns.example !== undefined && + app.actions[0].returns.example !== null && + app.actions[0].returns.example.length > 0 + ) { + example = app.actions[0].returns.example; + } + + if ( + app.actions[0].description !== undefined && + app.actions[0].description !== null && + app.actions[0].description.length > 0 + ) { + description = app.actions[0].description + } + + const parsedEnvironments = + environments === null || environments === [] + ? "cloud" + : environments[defaultEnvironmentIndex] === undefined + ? "cloud" + : environments[defaultEnvironmentIndex].Name; + + // activated: app.generated === true ? app.activated === false ? false : true : true, + const newAppData = { + name: app.actions[0].name, + label: actionLabel, + app_name: app.name, + app_version: app.app_version, + app_id: app.id, + sharing: app.sharing, + private_id: app.private_id, + description: description, + environment: parsedEnvironments, + errors: [], + finished: false, + id_: newNodeId, + _id_: newNodeId, + id: newNodeId, + is_valid: true, + type: actionType, + parameters: parameters, + isStartNode: false, + large_image: app.large_image, + run_magic_output: false, + authentication: [], + execution_variable: undefined, + example: example, + category: + app.categories !== null && + app.categories !== undefined && + app.categories.length > 0 + ? app.categories[0] + : "", + authentication_id: "", + finished: false, + template: app.template === true ? true : false, + }; + + // FIXME: overwrite category if the ACTION chosen has a different category + // + + if (!isCloud && (!app.is_valid || (!app.activated && app.generated))) { + console.log("NOT VALID: Activate!") + + activateApp(app.id, false) + } + + // const image = "url("+app.large_image+")" + // FIXME - find the cytoscape offset position + // Can this be done with zoom calculations? + const nodeToBeAdded = { + group: "nodes", + data: newAppData, + renderedPosition: { + x: e.pageX - cycontainer.offsetLeft, + y: e.pageY - cycontainer.offsetTop, + }, + }; + + parsedApp = nodeToBeAdded; + cy.add(nodeToBeAdded); + return; + } + } + }; + + const AppView = (props) => { + const { allApps, prioritizedApps, filteredApps, extraApps } = props; + //extraApps, + const [visibleApps, setVisibleApps] = React.useState( + Array.prototype.concat.apply( + prioritizedApps, + filteredApps.filter((innerapp) => !internalIds.includes(innerapp.id)), + ) + ); + + var delay = -75 + var runDelay = false + + const ParsedAppPaper = (props) => { + const app = props.app; + const [hover, setHover] = React.useState(false); + + if (app.id === "" || app.name === "") { + return null + } + + const maxlen = 24; + var newAppname = app.name; + newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1); + if (newAppname.length > maxlen) { + newAppname = newAppname.slice(0, maxlen) + ".."; + } + + newAppname = newAppname.replaceAll("_", " "); + + if (app.large_image === undefined || app.large_image === null || app.large_image === "") { + app.large_image = theme.palette.defaultImage + } + + const image = app.large_image !== undefined && app.large_image !== null && app.large_image !== "" ? app.large_image : theme.palette.defaultImage + + const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)); + const pixelSize = !hover ? "2px" : "4px"; + //newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 && !(app.activated && app.generated) + newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 + ? `${pixelSize} solid ${green}` + : `${pixelSize} solid ${yellow}`; + + if (!app.activated && app.generated) { + newAppStyle.borderLeft = `${pixelSize} solid ${yellow}`; + } + + return ( + { + handleAppDrag(e, app); + }} + onStop={(e) => { + handleDragStop(e, app); + }} + key={app.id} + dragging={false} + position={{ + x: 0, + y: 0, + }} + > + { + setHover(true); + }} + onMouseOut={() => { + setHover(false); + }} + onClick={() => { + if (isMobile) { + newNodeId = uuidv4(); + const actionType = "ACTION"; + const actionLabel = getNextActionName(app.name); + var parameters = null; + var example = ""; + var description = "" + + if ( + app.actions[0].parameters !== null && + app.actions[0].parameters.length > 0 + ) { + parameters = app.actions[0].parameters; + } + + if ( + app.actions[0].returns.example !== undefined && + app.actions[0].returns.example !== null && + app.actions[0].returns.example.length > 0 + ) { + example = app.actions[0].returns.example; + } + + if ( + app.actions[0].description !== undefined && + app.actions[0].description !== null && + app.actions[0].description.length > 0 + ) { + description = app.actions[0].description + } + + const parsedEnvironments = + environments === null || environments === [] + ? "cloud" + : environments[defaultEnvironmentIndex] === undefined + ? "cloud" + : environments[defaultEnvironmentIndex].Name; + + // activated: app.generated === true ? app.activated === false ? false : true : true, + const newAppData = { + app_name: app.name, + app_version: app.app_version, + app_id: app.id, + sharing: app.sharing, + private_id: app.private_id, + description: description, + environment: parsedEnvironments, + errors: [], + finished: false, + id_: newNodeId, + _id_: newNodeId, + id: newNodeId, + is_valid: true, + label: actionLabel, + type: actionType, + name: app.actions[0].name, + parameters: parameters, + isStartNode: false, + large_image: image, + run_magic_output: false, + authentication: [], + execution_variable: undefined, + example: example, + category: + app.categories !== null && + app.categories !== undefined && + app.categories.length > 0 + ? app.categories[0] + : "", + authentication_id: "", + finished: false, + }; + + const nodeToBeAdded = { + group: "nodes", + data: newAppData, + renderedPosition: { + x: 100, + y: 100, + }, + }; + + parsedApp = nodeToBeAdded; + cy.add(nodeToBeAdded); + + } + }} + > + + + {newAppname} + + {isMobile ? null : + + + + {newAppname} + + + + + Version: {app.app_version} + + + + + {app.description} + + + + } + + + + ); + }; + + const runSearch = (value) => { + if (value.length > 0) { + var newApps = allApps.filter( + (app) => + app.name.toLowerCase().includes(value.trim().toLowerCase()) + || + app.description.toLowerCase().includes(value.trim().toLowerCase()) + ) + + // Extend search + if (newApps.length === 0) { + const searchvalue = value.trim().toLowerCase(); + newApps = allApps.filter((app) => { + if (app.actions !== undefined && app.actions !== null) { + for (let actionkey in app.actions) { + const inneraction = app.actions[actionkey]; + if (inneraction.name.toLowerCase().includes(searchvalue)) { + return true; + } } } - - workflow.triggers[selectedTriggerIndex].parameters[1]["value_replace"] = paramcheck - setSelectedAction(selectedAction) - setUpdate(Math.random()) - setShowDropdown(false) - setMenuPosition(null) - return + return false; + }); + } + + setVisibleApps(newApps); + } else { + setVisibleApps( + prioritizedApps.concat( + filteredApps.filter( + (innerapp) => !internalIds.includes(innerapp.id) + ) + ) + ); + } + }; + + const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => { + + useEffect(() => { + if (document !== undefined) { + const appsearchValue = document.getElementById("appsearch") + if (appsearchValue !== undefined && appsearchValue !== null) { + if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { + refine(appsearchValue.value) + } + } + //} + } + }, []) + + return ( +