diff --git a/.env b/.env index 07deaf4a..b0a6a6cc 100755 --- a/.env +++ b/.env @@ -71,8 +71,8 @@ SHUFFLE_SWARM_BRIDGE_DEFAULT_MTU=1500 # 1500 by default # the container to the docker0 SHUFFLE_SWARM_BRIDGE_DEFAULT_INTERFACE=eth0 -# Used for auto-cleanup of containers. REALLY important at scale. -SHUFFLE_CONTAINER_AUTO_CLEANUP=false +# Used for auto-cleanup of containers. REALLY important at scale. Set to false to see all container info. +SHUFFLE_CONTAINER_AUTO_CLEANUP=true SHUFFLE_ELASTIC=true SHUFFLE_LOGS_DISABLED=false SHUFFLE_CHAT_DISABLED=false # Controls support chat diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index d22bc865..4870d614 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -3519,7 +3519,7 @@ class AppBase: if self.action["app_name"].lower() == "shuffle tools": timeout = 55 - timeout = 30 + #timeout = 30 try: executor = concurrent.futures.ThreadPoolExecutor() diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index fd2035c1..a76dff79 100755 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -1,6 +1,6 @@ module shuffle-shared -//replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared +replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared go 1.19 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 34b2991d..38c4b938 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -84,62 +84,8 @@ var registryName = "registry.hub.docker.com" var runningEnvironment = "onprem" var syncUrl = "https://shuffler.io" -var syncSubUrl = "https://shuffler.io" var dbclient *datastore.Client - -type Userapi struct { - Username string `datastore:"username"` - ApiKey string `datastore:"apikey"` -} - -type ExecutionInfo struct { - TotalApiUsage int64 `json:"total_api_usage" datastore:"total_api_usage"` - TotalWorkflowExecutions int64 `json:"total_workflow_executions" datastore:"total_workflow_executions"` - TotalAppExecutions int64 `json:"total_app_executions" datastore:"total_app_executions"` - TotalCloudExecutions int64 `json:"total_cloud_executions" datastore:"total_cloud_executions"` - TotalOnpremExecutions int64 `json:"total_onprem_executions" datastore:"total_onprem_executions"` - DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - DailyWorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` - DailyAppExecutions int64 `json:"daily_app_executions" datastore:"daily_app_executions"` - DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` - DailyOnpremExecutions int64 `json:"daily_onprem_executions" datastore:"daily_onprem_executions"` -} - -// "Execution by status" -// Execution history -//type GlobalStatistics struct { -// BackendExecutions int64 `json:"backend_executions" datastore:"backend_executions"` -// WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` -// ExecutionCount int64 `json:"execution_count" datastore:"execution_count"` -// ExecutionSuccessCount int64 `json:"execution_success_count" datastore:"execution_success_count"` -// ExecutionAbortCount int64 `json:"execution_abort_count" datastore:"execution_abort_count"` -// ExecutionFailureCount int64 `json:"execution_failure_count" datastore:"execution_failure_count"` -// ExecutionPendingCount int64 `json:"execution_pending_count" datastore:"execution_pending_count"` -// AppUsageCount int64 `json:"app_usage_count" datastore:"app_usage_count"` -// TotalAppsCount int64 `json:"total_apps_count" datastore:"total_apps_count"` -// SelfMadeAppCount int64 `json:"self_made_app_count" datastore:"self_made_app_count"` -// WebhookUsageCount int64 `json:"webhook_usage_count" datastore:"webhook_usage_count"` -// Baseline map[string]int64 `json:"baseline" datastore:"baseline"` -//} - -type ParsedOpenApi struct { - Body string `datastore:"body,noindex" json:"body"` - ID string `datastore:"id" json:"id"` - Success bool `datastore:"success,omitempty" json:"success,omitempty"` -} - -// Limits set for a user so that they can't do a shitload -type UserLimits struct { - DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - DailyWorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` - DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` - DailyTriggers int64 `json:"daily_triggers" datastore:"daily_triggers"` - DailyMailUsage int64 `json:"daily_mail_usage" datastore:"daily_mail_usage"` - MaxTriggers int64 `json:"max_triggers" datastore:"max_triggers"` - MaxWorkflows int64 `json:"max_workflows" datastore:"max_workflows"` -} - type retStruct struct { Success bool `json:"success"` SyncFeatures shuffle.SyncFeatures `json:"sync_features"` @@ -148,43 +94,6 @@ type retStruct struct { Reason string `json:"reason"` } -// Saves some data, not sure what to have here lol -type UserAuth struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Name string `json:"name" datastore:"name" yaml:"name"` - Workflows []string `json:"workflows" datastore:"workflows"` - Username string `json:"username" datastore:"username"` - Fields []UserAuthField `json:"fields" datastore:"fields"` -} - -type UserAuthField struct { - Key string `json:"key" datastore:"key"` - Value string `json:"value" datastore:"value,noindex"` -} - -// Not environment, but execution environment -//type Environment struct { -// Name string `datastore:"name"` -// Type string `datastore:"type"` -// Registered bool `datastore:"registered"` -// Default bool `datastore:"default" json:"default"` -// Archived bool `datastore:"archived" json:"archived"` -// Id string `datastore:"id" json:"id"` -// OrgId string `datastore:"org_id" json:"org_id"` -//} - -// timeout maybe? idk -type session struct { - Username string `datastore:"Username,noindex"` - Id string `datastore:"Id,noindex"` - Session string `datastore:"session,noindex"` -} - -type loginStruct struct { - Username string `json:"username"` - Password string `json:"password"` -} - type Contact struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` @@ -1102,7 +1011,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { userOrgs = shuffle.SortOrgList(userOrgs) orgPriorities := org.Priorities if len(org.Priorities) < 10 { - log.Printf("[WARNING] Should find and add priorities as length is less than 10 for org %s", userInfo.ActiveOrg.Id) + //log.Printf("[WARNING] Should find and add priorities as length is less than 10 for org %s", userInfo.ActiveOrg.Id) newPriorities, err := shuffle.GetPriorities(ctx, userInfo, org) if err != nil { log.Printf("[WARNING] Failed getting new priorities for org %s: %s", org.Id, err) @@ -3750,16 +3659,69 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error { return nil } + func remoteOrgJobHandler(org shuffle.Org, interval int) error { + + // Check if it's 1 in 10 (10% chance random) + backupJob := shuffle.BackupJob{} + + // Check if workflow backup is active + // Check if app backup is active + ctx := context.Background() + + foundUser := org.Users[0] + for _, user := range org.Users { + if user.Role == "admin" { + foundUser = user + break + } + } + + if org.SyncConfig.WorkflowBackup { + workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser) + if err != nil { + log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) + } else { + backupJob.Workflows = workflows + } + } + + if org.SyncConfig.AppBackup && len(org.Users) > 0 { + + apps, err := shuffle.GetPrioritizedApps(ctx, foundUser) + if err != nil { + log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err) + } else { + backupJob.Apps = apps + } + } + + info, err := shuffle.GetOrgStatistics(ctx, org.Id) + if err != nil { + log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err) + } else { + backupJob.Stats = *info + } + + backupJobData, err := json.Marshal(backupJob) + if err != nil { + log.Printf("[ERROR] Failed marshalling backup job: %s", err) + backupJobData = []byte{} + } + + client := &http.Client{} syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) req, err := http.NewRequest( - "GET", + "POST", syncUrl, - nil, + bytes.NewBuffer(backupJobData), ) req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey)) + + //log.Printf("[INFO] Sending org sync with autho %s", org.SyncConfig.Apikey) + newresp, err := client.Do(req) if err != nil { //log.Printf("Failed request in org sync: %s", err) @@ -3775,7 +3737,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { //log.Printf("Remote Data: %s", respBody) err = remoteOrgJobController(org, respBody) if err != nil { - log.Printf("[ERROR] Failed job controller run for %s: %s", respBody, err) + //log.Printf("[ERROR] Failed cloud sync job controller run for '%s': %s", respBody, err) return err } return nil @@ -3819,7 +3781,7 @@ func runInitEs(ctx context.Context) { activeOrgs, err := shuffle.GetAllOrgs(ctx) setUsers := false - //log.Printf("ORGS: %d", len(activeOrgs)) + _ = setUsers if err != nil { if fmt.Sprintf("%s", err) == "EOF" { time.Sleep(7 * time.Second) @@ -3874,7 +3836,7 @@ func runInitEs(ctx context.Context) { if len(activeOrgs) == 1 { if len(activeOrgs[0].Users) == 0 { - log.Printf("ORG doesn't have any users??") + log.Printf("[ERROR] Main Org doesn't have any user. Creating.") users, err := shuffle.GetAllUsers(ctx) if err != nil && len(users) == 0 { @@ -3907,10 +3869,9 @@ func runInitEs(ctx context.Context) { if strings.Contains(os.Getenv("SHUFFLE_OPENSEARCH_URL"), "https") { log.Printf("[INFO] Waiting during init to make sure the opensearch instance is up and running with security features properly") - time.Sleep(30 * time.Second) + time.Sleep(15 * time.Second) } - _ = setUsers schedules, err := shuffle.GetAllSchedules(ctx, "ALL") if err != nil { log.Printf("[WARNING] Failed getting schedules during service init: %s", err) @@ -4054,11 +4015,11 @@ func runInitEs(ctx context.Context) { continue } - log.Printf("[DEBUG] Should start schedule for org %s (%s)", org.Name, org.Id) + log.Printf("[DEBUG] Should start cloud schedule for org %s (%s)", org.Name, org.Id) job := func() { err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("[ERROR] Failed request with remote org setup (2): %s", err) + log.Printf("[ERROR] Failed request with remote org setup for org %s (2): %s", org.Id, err) } } @@ -4760,7 +4721,7 @@ func runInit(ctx context.Context) { job := func() { err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("[ERROR] Failed request with remote org setup (2): %s", err) + log.Printf("[ERROR] Failed request with remote org setup (3): %s", err) } } @@ -5244,7 +5205,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { b, err := json.Marshal(requestData) if err != nil { - log.Printf("Failed marshaling api key data: %s", err) + log.Printf("[ERROR] Failed marshaling api key data: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed cloud sync: %s"}`, err))) return @@ -5271,7 +5232,8 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { return } - //log.Printf("Respbody: %s", string(respBody)) + log.Printf("[DEBUG] Respbody from sync: %s", string(respBody)) + responseData := retStruct{} err = json.Unmarshal(respBody, &responseData) if err != nil { @@ -5896,6 +5858,8 @@ func initHandlers() { r.HandleFunc("/api/v1/_ah/health", shuffle.HealthCheckHandler) r.HandleFunc("/api/v1/health", shuffle.RunOpsHealthCheck).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/health/stats", shuffle.GetOpsDashboardStats).Methods("GET", "OPTIONS") + // Make user related locations // Fix user changes with org r.HandleFunc("/api/v1/users/login", shuffle.HandleLogin).Methods("POST", "OPTIONS") @@ -5967,10 +5931,6 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS") - // Related to NFT things - r.HandleFunc("/api/v1/workflows/collections/load", shuffle.LoadCollections).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/workflows/collections/{key}", shuffle.HandleGetCollection).Methods("GET", "OPTIONS") - // Related to use-cases that are not directly workflows. r.HandleFunc("/api/v1/workflows/usecases/{key}", shuffle.HandleGetUsecase).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS") @@ -6000,6 +5960,9 @@ func initHandlers() { r.HandleFunc("/api/v1/workflows/{key}", shuffle.GetSpecificWorkflow).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/recommend", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS") + // First v2 API + r.HandleFunc("/api/v2/workflows/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS") + // New for recommendations in Shuffle r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/recommendations/modify", shuffle.HandleRecommendationAction).Methods("POST", "OPTIONS") @@ -6114,7 +6077,6 @@ func initHandlers() { func main() { initHandlers() - go shuffle.InitOpsWorkflow() hostname, err := os.Hostname() if err != nil { hostname = "MISSING" diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index d98f922b..4c29f71e 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -700,64 +700,6 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } } - /* - // Removed as UserInput is now handled as an app - if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" { - log.Printf("[INFO] SHOULD WAIT A BIT AND RUN USER INPUT! WAITING!") - - var trigger shuffle.Trigger - err = json.Unmarshal([]byte(actionResult.Result), &trigger) - if err != nil { - log.Printf("[WARNING] Failed unmarshaling actionresult for user input: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - orgId := workflowExecution.ExecutionOrg - if len(workflowExecution.OrgId) == 0 && len(workflowExecution.Workflow.OrgId) > 0 { - orgId = workflowExecution.Workflow.OrgId - } - - err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId) - if err != nil { - log.Printf("[WARNING] Failed userinput handler: %s", err) - - actionResult.Result = fmt.Sprintf(`{"success": false, "reason": "%s"}`, err) - - workflowExecution.Results = append(workflowExecution.Results, actionResult) - workflowExecution.Status = "ABORTED" - err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) - if err != nil { - log.Printf("[WARNING] Failed to set execution during wait: %s", err) - } else { - log.Printf("[INFO] Successfully set the execution %s to waiting.", workflowExecution.ExecutionId) - } - - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err))) - return - } else { - log.Printf("[INFO] Successful userinput handler") - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "CLOUD IS DONE"}`))) - - actionResult.Result = `{"success": True, "reason": "Waiting for user feedback based on configuration"}` - - workflowExecution.Results = append(workflowExecution.Results, actionResult) - workflowExecution.Status = actionResult.Status - err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) - if err != nil { - log.Printf("[WARNING] Failed setting userinput: %s", err) - } else { - log.Printf("[DEBUG] Successfully set the execution to waiting.") - } - } - - return - } - */ - runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) } @@ -1750,6 +1692,8 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request return shuffle.WorkflowExecution{}, "Cloud not implemented yet", errors.New("Cloud not implemented yet") } + shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions_cloud") + // What it needs to know: // 1. Parameters if len(workflowExecution.Workflow.Actions) == 1 { @@ -1763,13 +1707,11 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request // If worker, should this backend be a proxy? I think so. return shuffle.WorkflowExecution{}, "Cloud not implemented yet (2)", errors.New("Cloud not implemented yet") } + } else { + shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions_onprem") } - //err = increaseStatisticsField(ctx, "workflow_executions", workflow.ID, 1, workflowExecution.ExecutionOrg) - //if err != nil { - // log.Printf("Failed to increase stats execution stats: %s", err) - //} - + shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions") return workflowExecution, "", nil } diff --git a/docker-compose.yml b/docker-compose.yml index 2eb61619..1b93335c 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - image: ghcr.io/shuffle/shuffle-frontend:latest + image: ghcr.io/shuffle/shuffle-frontend:nightly container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -61,8 +61,9 @@ services: hostname: shuffle-opensearch container_name: shuffle-opensearch environment: - - bootstrap.memory_lock=true - "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM + - bootstrap.memory_lock=true + - DISABLE_PERFORMANCE_ANALYZER_AGENT_CLI=true - cluster.initial_master_nodes=shuffle-opensearch - cluster.routing.allocation.disk.threshold_enabled=false - cluster.name=shuffle-cluster diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2180be03..f2f7a06a 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -326,6 +326,7 @@ const App = (message, props) => { path="/usecases" element={ `), "CASES": encodeURI(`data:image/svg+xml;utf-8,`), @@ -713,7 +713,7 @@ const AppFramework = (props) => { } useEffect(() => { - console.log("DISCWRAP CHANG: ", discoveryWrapper) + //console.log("DISCWRAP CHANG: ", discoveryWrapper) if (discoveryWrapper === undefined || discoveryWrapper.id === "SHUFFLE" || discoveryWrapper.id === undefined || cy === undefined) { setDiscoveryData({}) @@ -874,7 +874,7 @@ const AppFramework = (props) => { }, []) useEffect(() => { - console.log("New selected app: ", newSelectedApp, discoveryData) + //console.log("New selected app: ", newSelectedApp, discoveryData) if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) { return } @@ -1856,8 +1856,6 @@ const AppFramework = (props) => { //autounselectify={true} var usecasediff = -100 const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color - console.log("Background: ", bgColor) - return (
@@ -2067,8 +2065,8 @@ const AppFramework = (props) => { const foundelement = cy.getElementById(discoveryData.id) if (foundelement !== undefined && foundelement !== null) { - console.log("element: ", foundelement) - console.log("DISC: ", discoveryData) + //console.log("element: ", foundelement) + //console.log("DISC: ", discoveryData) foundelement.data("large_image", parsedDatatypeImages[discoveryData.id.toUpperCase()]) foundelement.data("text_margin_y", "14px") foundelement.data("margin_x", "32px") diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index 7408dad5..87851541 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -200,7 +200,7 @@ const Appsearch = props => { const CustomHits = connectHits(InputHits) return ( -
+
{/* showSearch === false ? null :
diff --git a/frontend/src/components/AuthenticationItem.jsx b/frontend/src/components/AuthenticationItem.jsx index 593eacb4..eae08e69 100644 --- a/frontend/src/components/AuthenticationItem.jsx +++ b/frontend/src/components/AuthenticationItem.jsx @@ -18,7 +18,6 @@ import { Grid, Paper, Typography, - TextField, Zoom, } from "@mui/material"; diff --git a/frontend/src/components/AuthenticationWindow.jsx b/frontend/src/components/AuthenticationWindow.jsx index d7bcc55a..7d32f597 100755 --- a/frontend/src/components/AuthenticationWindow.jsx +++ b/frontend/src/components/AuthenticationWindow.jsx @@ -294,9 +294,6 @@ const AuthenticationData = (props) => { InputProps={{ style: { color: "white", - marginLeft: "5px", - maxWidth: "95%", - height: 50, fontSize: "1em", }, disableUnderline: true, @@ -386,7 +383,6 @@ const AuthenticationData = (props) => { PaperProps={{ style: { pointerEvents: "auto", - backgroundColor: theme.palette.surfaceColor, color: "white", minWidth: 600, minHeight: 600, @@ -441,9 +437,6 @@ const AuthenticationData = (props) => { InputProps={{ style: { color: "white", - marginLeft: "5px", - maxWidth: "95%", - height: 50, fontSize: "1em", }, }} diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 71d01789..0b9c484d 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -1,316 +1,1010 @@ -import React, { useState, useEffect } from "react"; -import theme from "../theme.jsx"; -import ReactGA from 'react-ga4'; - -import { - Paper, - Typography, - Divider, - Button, - Grid, - Card, -} from "@mui/material"; - -import { useAlert } from "react-alert"; -import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; - -const Billing = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props; - console.log("Billing: ", billingInfo); - const alert = useAlert(); - - const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : "" - console.log("Stripe: ", stripe) - - const paperStyle = { - padding: 20, - height: "100%", - width: "100%", - backgroundColor: theme.palette.surfaceColor, - border: "1px solid rgba(255,255,255,0.3)", - marginRight: 10, - } - - const isCloud = - window.location.host === "localhost:3002" || - window.location.host === "shuffler.io"; - - billingInfo.subscription = { - "active": true, - "name": "Pay as you go", - "price": typecost_single, - "currency": "USD", - "currency_text": "$", - "interval": "app run / month", - "description": "Pay as you go", - "features": [ - "Includes 10.000 app run/month for free. ", - "Pay for what you use with no minimum commitment and cancel anytime.", - ], - "limit": 10000, - } - - - const handleStripeRedirect = () => { - //var priceItem = "price_1MRNF1DzMUgUjxHSfFTUb2Xh" - if (stripe == "") { - console.log("Stripe not loaded") - return - } - - var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1" - - const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` - const failUrl = `${window.location.origin}/admin?admin_tab=billing&payment=failure` - var checkoutObject = { - lineItems: [ - { - price: priceItem, - quantity: 1 - }, - ], - mode: "subscription", - billingAddressCollection: "auto", - successUrl: successUrl, - cancelUrl: failUrl, - clientReferenceId: props.userdata.active_org.id, - } - //submitType: "donate", - - stripe.redirectToCheckout(checkoutObject) - .then(function (result) { - console.log("SUCCESS STRIPE?: ", result) - - ReactGA.event({ - category: "pricing", - action: "add_card_success", - label: "", - }) - }) - .catch(function(error) { - console.error("STRIPE ERROR: ", error) - - ReactGA.event({ - category: "pricing", - action: "add_card_error", - label: "", - }) - }); - } - - const cancelSubscriptions = (subscription_id) => { - const orgId = selectedOrganization.id; - const data = { - subscription_id: subscription_id, - action: "cancel", - org_id: selectedOrganization.id, - }; - - const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } - - if (handleGetOrg != undefined) { - handleGetOrg(selectedOrganization.id); - } - - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success !== undefined && responseJson.success) { - alert.success("Successfully stopped subscription!"); - } 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 SubscriptionObject = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, } = props; - - console.log("Sub: ", subscription) - var top_text = "Base Access" - if (subscription.limit === undefined && subscription.level !== undefined) { - - subscription.name = "Enterprise" - subscription.currency_text = "$" - subscription.price = subscription.level*180 - subscription.limit = subscription.level*100000 - subscription.interval = subscription.recurrence - subscription.features = [ - "Includes " + subscription.limit + " app runs/month. ", - "Multi-Tenancy and Region-Selection", - "And all other features from /pricing", - ] - } - - if (subscription.name === "Enterprise" && subscription.active === true) { - top_text = "Current Plan" - } - - return ( - -
- - {top_text} - -
- -
- - {subscription.name} - -
- - {subscription.currency_text}{subscription.price} - - - / {subscription.interval} - -
- - Features - -
    - {subscription.features !== undefined && subscription.features !== null ? - subscription.features.map((feature, index) => { - return ( -
  • - - {feature} - -
  • - ) - }) - : null} -
-
- {/*subscription.name === "Pay as you go" && subscription.limit <= 10000 ? - - - You are not subscribed to any plan and are using the free plan with max 10,000 apps per month. Activate billing to de-activate this limit. - - - - : null*/} -
- ) - } - - - return ( -
- - Billing - - - We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below. - -
- {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? - - : null} - {isCloud && - selectedOrganization.subscriptions !== undefined && - selectedOrganization.subscriptions !== null && - selectedOrganization.subscriptions.length > 0 ? - - selectedOrganization.subscriptions - .reverse() - .map((sub, index) => { - return ( - - ) - }) - : null} - {/* - - - Quantity: {sub.level} -
- Recurrence: {sub.recurrence} -
- {sub.active ? ( -
- Started:{" "} - {new Date(sub.startdate * 1000).toISOString()} -
- -
- ) : ( -
- Cancelled:{" "} - {new Date( - sub.cancellationdate * 1000 - ).toISOString()} -
- - Status: Deactivated - -
- )} - - - */} -
-
- ) -} - -export default Billing; +import React, { useState, useEffect } from "react"; +import ReactGA from 'react-ga4'; + +import theme from "../theme.jsx"; +import { useTheme } from "@mui/styles"; +import countries from "../components/Countries.jsx"; +import { + Box, + Paper, + Typography, + Divider, + Button, + Grid, + Card, + List, + ListItemText, + ListItem, + Dialog, + DialogTitle, + DialogContent, + TextField, +} from "@mui/material"; + +import { useNavigate, Link } from "react-router-dom"; +import { Autocomplete } from "@mui/material"; +import { toast } from "react-toastify" + +import { + Cached as CachedIcon, +} from "@mui/icons-material"; + +//import { useAlert +import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; +import BillingStats from "../components/BillingStats.jsx"; + +const Billing = (props) => { + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props; + //const alert = useAlert(); + let navigate = useNavigate(); + + const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); + const [dealList, setDealList] = React.useState([]); + const [dealName, setDealName] = React.useState(""); + const [dealAddress, setDealAddress] = React.useState(""); + const [dealType, setDealType] = React.useState("MSSP"); + const [dealCountry, setDealCountry] = React.useState("United States"); + const [dealCurrency, setDealCurrency] = React.useState("USD"); + const [dealStatus, setDealStatus] = React.useState("initiated"); + const [dealValue, setDealValue] = React.useState(""); + const [dealDiscount, setDealDiscount] = React.useState(""); + const [dealerror, setDealerror] = React.useState(""); + + const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : "" + const products = [ + { code: "", label: "MSSP", phone: "" }, + { code: "", label: "Enterprise", phone: "" }, + { code: "", label: "Consultancy", phone: "" }, + { code: "", label: "Support", phone: "" }, + ]; + + const handleGetDeals = (orgId) => { + console.log("Get deals!"); + + if (orgId.length === 0) { + toast( + "Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout." + ); + return; + } + + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Bad status code in get deals: ", response.status); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got deals: ", responseJson); + if (responseJson.success === false) { + toast("Failed loading deals. Contact support if this persists"); + } else { + setDealList(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org deals: ", error); + toast( + "Failed getting deals for your org. Contact support if this persists." + ); + }); + }; + + useEffect(() => { + if (isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true) { + handleGetDeals(selectedOrganization.id); + } + }, []) + + const paperStyle = { + padding: 20, + height: "100%", + minHeight: 280, + maxWidth: 400, + width: "100%", + backgroundColor: theme.palette.surfaceColor, + borderRadius: theme.palette.borderRadius, + border: "1px solid rgba(255,255,255,0.3)", + marginRight: 10, + } + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + billingInfo.subscription = { + "active": true, + "name": "Pay as you go", + "price": typecost_single, + "currency": "USD", + "currency_text": "$", + "interval": "app run / month", + "description": "Pay as you go", + "features": [ + "Includes 10.000 app run/month for free. ", + "Pay for what you use with no minimum commitment and cancel anytime.", + ], + "limit": 10000, + } + + + const handleStripeRedirect = () => { + //var priceItem = "price_1MRNF1DzMUgUjxHSfFTUb2Xh" + if (stripe == "") { + console.log("Stripe not loaded") + return + } + + var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1" + + const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` + const failUrl = `${window.location.origin}/admin?admin_tab=billing&payment=failure` + var checkoutObject = { + lineItems: [ + { + price: priceItem, + quantity: 1 + }, + ], + mode: "subscription", + billingAddressCollection: "auto", + successUrl: successUrl, + cancelUrl: failUrl, + clientReferenceId: props.userdata.active_org.id, + } + //submitType: "donate", + + stripe.redirectToCheckout(checkoutObject) + .then(function (result) { + console.log("SUCCESS STRIPE?: ", result) + + ReactGA.event({ + category: "pricing", + action: "add_card_success", + label: "", + }) + }) + .catch(function(error) { + console.error("STRIPE ERROR: ", error) + + ReactGA.event({ + category: "pricing", + action: "add_card_error", + label: "", + }) + }); + } + + const cancelSubscriptions = (subscription_id) => { + const orgId = selectedOrganization.id; + const data = { + subscription_id: subscription_id, + action: "cancel", + org_id: selectedOrganization.id, + }; + + const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } + + if (handleGetOrg != undefined) { + handleGetOrg(selectedOrganization.id); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success !== undefined && responseJson.success) { + toast("Successfully stopped subscription!"); + } else { + toast("Failed stopping subscription. Please contact us."); + } + }) + .catch(function (error) { + console.log("Error: ", error); + toast("Failed stopping subscription. Please contact us."); + }); + }; + + const SubscriptionObject = (props) => { + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; + + var top_text = "Base Access" + if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { + subscription.name = "Enterprise" + subscription.currency_text = "$" + subscription.price = subscription.level*180 + subscription.limit = subscription.level*100000 + subscription.interval = subscription.recurrence + subscription.features = [ + "Includes " + subscription.limit + " app runs/month. ", + "Multi-Tenancy and Region-Selection", + "And all other features from /pricing", + ] + } + + var newPaperstyle = JSON.parse(JSON.stringify(paperStyle)) + if (subscription.name === "Enterprise" && subscription.active === true) { + top_text = "Current Plan" + + newPaperstyle.border = "1px solid #f85a3e" + } + + var showSupport = false + if (subscription.name.includes("default")) { + top_text = "Custom Contract" + newPaperstyle.border = "1px solid #f85a3e" + showSupport = true + } + + if (subscription.name.includes("App Run Units")) { + top_text = "Cloud Access" + showSupport = true + } + + if (subscription.name.includes("Open Source")) { + top_text = "Open Source" + showSupport = true + } + + if (subscription.name.includes("Scale")) { + top_text = "Scale access" + } + + if (highlight === true) { + // Add an "Upgrade now" button + newPaperstyle.border = "1px solid #f85a3e" + } + + return ( + +
+ + {top_text} + +
+ +
+ + {subscription.name} + + + {subscription.currency_text !== undefined ? +
+ + {subscription.currency_text}{subscription.price} + + + / {subscription.interval} + +
+ : null} + + + Features + +
    + {subscription.features !== undefined && subscription.features !== null ? + subscription.features.map((feature, index) => { + var parsedFeature = feature + if (feature.includes("Documentation: ")) { + parsedFeature = + + Documentation to get started + + } + + if (feature.includes("Licensed Worker: ")) { + parsedFeature = + + Download the licensed worker + + } + + return ( +
  • + + {parsedFeature} + +
  • + ) + }) + : null} +
+
+ {(highlight === true && subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name.includes("Scale") ? + + + {subscription.name.includes("Scale") ? + "" + : + "You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit." + } + + + + : null} + {showSupport ? + + : null } +
+ ) + } + + const addDealModal = ( + { + setSelectedDealModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + Register new deal + + +
+ { + setDealName(e.target.value); + }} + /> + { + setDealAddress(e.target.value); + }} + /> +
+
+ { + setDealValue(e.target.value); + }} + /> + option.label} + onChange={(event, newValue) => { + setDealCountry(newValue.label); + }} + renderOption={(props, option) => ( + img": { mr: 2, flexShrink: 0 } }} + {...props} + > + + {option.label} ({option.code}) +{option.phone} + + )} + renderInput={(params) => ( + + )} + /> + { + setDealType(newValue); + }} + getOptionLabel={(option) => option.label} + renderOption={(props, option) => ( + img": { mr: 2, flexShrink: 0 } }} + {...props} + > + {option.label} + + )} + renderInput={(params) => ( + + )} + /> +
+ {dealerror.length > 0 ? ( + + error registering: {dealerror} + + ) : null} +
+ + +
+
+
+ ); + + const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => { + if (dealerror.length > 0) { + setDealerror(""); + } + + const orgId = selectedOrganization.id; + const data = { + reseller_org: orgId, + name: dealName, + address: dealAddress, + country: dealCountry, + value: dealValue, + }; + + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + setSelectedDealModalOpen(false); + toast( + "Added new deal! We will be in touch shortly with an update." + ); + + setDealName(""); + setDealAddress(""); + setDealValue(""); + setDealCountry("United States"); + setDealType("MSSP"); + } else { + setDealerror(responseJson.reason); + } + }) + .catch(function (error) { + //console.log("Error: ", error); + setDealerror(error.toString()); + toast("Failed adding deal reg: ", error); + }); + }; + + return ( +
+ {addDealModal} + + Billing + + + {isCloud ? + "We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." + : + "Shuffle is an Open Source automation platform, and no license is required to use it. You may however activate Cloud Sync, get our Scale license, get help with Kubernetes, or talk to Shuffle's Support team to get automation help." + } + +
+ {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? + + : !isCloud ? + + + + + : null} + {isCloud && + selectedOrganization.subscriptions !== undefined && + selectedOrganization.subscriptions !== null && + selectedOrganization.subscriptions.length > 0 ? + + selectedOrganization.subscriptions + .reverse() + .map((sub, index) => { + return ( + + ) + }) + : null} + {/* + + + Quantity: {sub.level} +
+ Recurrence: {sub.recurrence} +
+ {sub.active ? ( +
+ Started:{" "} + {new Date(sub.startdate * 1000).toISOString()} +
+ +
+ ) : ( +
+ Cancelled:{" "} + {new Date( + sub.cancellationdate * 1000 + ).toISOString()} +
+ + Status: Deactivated + +
+ )} + + + */} +
+ {isCloud && + selectedOrganization.partner_info !== undefined && + selectedOrganization.partner_info.reseller === true ? ( +
+ + Reseller dashboard + + + + + + + + + + + + + + + + + + {dealList.length === 0 ? ( + + No deals registered yet. Click "Add deal" to register one + + ) : ( + dealList.map((deal, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + + + + + + + + + ); + }) + )} + + + + +
+ ) : null} +
+ + Billing Usage Overview + +
+ +
+ ) +} + +export default Billing; diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx new file mode 100644 index 00000000..3f7a7516 --- /dev/null +++ b/frontend/src/components/BillingStats.jsx @@ -0,0 +1,280 @@ +import React, { useState, useEffect } from 'react'; + +import classNames from "classnames"; +import theme from '../theme.jsx'; + +import { + Tooltip, + TextField, + IconButton, + Button, + Typography, + Grid, + Paper, + Chip, + Checkbox, +} from "@mui/material"; + +import { + BarChart, + RadialBarChart, + RadialAreaChart, + RadialAxis, + StackedBarSeries, + TooltipArea, + ChartTooltip, + TooltipTemplate, + RadialAreaSeries, + RadialPointSeries, + RadialArea, + RadialLine, + TreeMap, + TreeMapSeries, + TreeMapLabel, + TreeMapRect, + Line, + LineChart, + LineSeries, + LinearYAxis, + LinearXAxis, + LinearYAxisTickSeries, + LinearXAxisTickSeries, + Area, + AreaChart, + AreaSeries, + AreaSparklineChart, + PointSeries, + GridlineSeries, + Gridline, + Stripes, + Gradient, + GradientStop, + LinearXAxisTickLabel, +} from 'reaviz'; + +const LineChartWrapper = ({keys, inputname, height, width}) => { + const [hovered, setHovered] = useState(""); + const inputdata = keys.data === undefined ? keys : keys.data + + return ( +
+ + {inputname} + + } /> + } + /> +
+ ) +} + + +const AppStats = (defaultprops) => { + const { globalUrl, selectedOrganization, userdata, } = defaultprops; + const [keys, setKeys] = useState([]) + const [searches, setSearches] = useState([]); + const [clickData, setClickData] = useState(undefined); + const [conversionData, setConversionData] = useState(undefined); + const [statistics, setStatistics] = useState(undefined); + const [appRuns, setAppruns] = useState(undefined); + const [workflowRuns, setWorkflowRuns] = useState(undefined); + const [subflowRuns, setSubflowRuns] = useState(undefined); + + const handleDataSetting = (inputdata, grouping) => { + if (inputdata === undefined || inputdata === null) { + return + } + + const dailyStats = inputdata.daily_statistics + if (dailyStats === undefined || dailyStats === null) { + return + } + + console.log("Looking at daily data: ", inputdata) + + var appRuns = { + "key": "App Runs", + "data": [] + } + + var workflowRuns = { + "key": "Workflow Runs (includes subflows)", + "data": [] + } + + var subflowRuns = { + "key": "Subflow Runs", + "data": [] + } + + for (let key in dailyStats) { + // Always skips first one as it has accumulated data in it + if (key === 0) { + continue + } + + const item = dailyStats[key] + + if (item["date"] === undefined) { + console.log("No date: ", item) + continue + } + + // Check if app_executions key in item + if (item["app_executions"] !== undefined && item["app_executions"] !== null) { + appRuns["data"].push({ + key: new Date(item["date"]), + data: item["app_executions"] + }) + } + + // Check if workflow_executions key in item + if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) { + workflowRuns["data"].push({ + key: new Date(item["date"]), + data: item["workflow_executions"] + }) + } + + if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) { + subflowRuns["data"].push({ + key: new Date(item["date"]), + data: item["subflow_executions"] + }) + } + } + + // Adds data for today + console.log("Inputdata: ", inputdata) + if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { + appRuns["data"].push({ + key: new Date(), + data: inputdata["daily_app_executions"] + }) + } + + if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { + workflowRuns["data"].push({ + key: new Date(), + data: inputdata["daily_workflow_executions"] + }) + } + + if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) { + subflowRuns["data"].push({ + key: new Date(), + data: inputdata["daily_subflow_executions"] + }) + } + + setSubflowRuns(subflowRuns) + setWorkflowRuns(workflowRuns) + setAppruns(appRuns) + } + + const getStats = () => { + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, { + 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!: ", response.status); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + return + } + + setStatistics(responseJson) + handleDataSetting(responseJson, "day") + }) + .catch((error) => { + console.log("error: ", error) + }); + } + + useEffect(() => { + getStats() + }, []) + + const paperStyle = { + textAlign: "center", + padding: 40, + margin: 5, + backgroundColor: theme.palette.surfaceColor, + maxWidth: 300, + } + + const data = ( +
+ + All Stat widgets are monthly and gathered from Your Organization Statistics. + This is a feature to help give you more insight into Shuffle, and will be populating over time. + + {statistics !== undefined ? +
+ + + {statistics.monthly_workflow_executions} + + + Workflow Runs + + + + + {statistics.monthly_app_executions} + + + App Runs + + +
+ : null} + + {appRuns === undefined ? + null + : + + } + + {workflowRuns === undefined ? + null + : + + } + + {subflowRuns === undefined ? + null + : + + } +
+ ) + + const dataWrapper = ( +
{data}
+ ); + + return dataWrapper; +} + +export default AppStats; diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index 0e80540c..52a3ac1b 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -16,11 +16,17 @@ import { ListItem, ListItemText, Collapse, + IconButton, } from "@mui/material"; + import { FavoriteBorder as FavoriteBorderIcon, Error as ErrorIcon, CheckCircleRounded as CheckCircleRoundedIcon, + ExpandMore as ExpandMoreIcon, + ExpandLess as ExpandLessIcon, + Check as CheckIcon, + Visibility as VisibilityIcon, } from "@mui/icons-material"; import { FixName } from "../views/Apps.jsx"; import aa from 'search-insights' @@ -34,27 +40,26 @@ import aa from 'search-insights' // Specifically used for UNSAVED workflows only? const ConfigureWorkflow = (props) => { const { - userdata, - globalUrl, + apps, theme, + isCloud, workflow, + userdata, + globalUrl, + newWebhook, + referenceUrl, + saveWorkflow, + showTriggers, + submitSchedule, + setSelectedApp, + selectedAction, appAuthentication, setSelectedAction, - setAuthenticationModalOpen, - setSelectedApp, - apps, - selectedAction, - setConfigureWorkflowModalOpen, - saveWorkflow, - newWebhook, - submitSchedule, - referenceUrl, - isCloud, + workflowExecutions, + getWorkflowExecution, setAuthenticationType, - alert, - showTriggers, - workflowExecutions, - getWorkflowExecution, + setAuthenticationModalOpen, + setConfigureWorkflowModalOpen, } = props; const [requiredActions, setRequiredActions] = React.useState([]); @@ -64,9 +69,39 @@ const ConfigureWorkflow = (props) => { const [itemChanged, setItemChanged] = React.useState(false); const [firstLoad, setFirstLoad] = React.useState(""); const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false); + const [loopRunning, setLoopRunning] = useState(false) - const [checkStarted, setCheckStarted] = React.useState(false); + const [checkStarted, setCheckStarted] = React.useState(false); + const stop = () => { + setLoopRunning(false) + } + + const start = () => { + setLoopRunning(true) + } + + useEffect(() => { + if (loopRunning) { + const intervalId = setInterval(() => { + if (!loopRunning) { + clearInterval(intervalId); + } + + + if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) { + const paramkey = workflow.id + getWorkflowExecution(paramkey) + } else { + console.log("Executions or getWorkflowExecutions not defined") + } + }, 3000) + + return () => clearInterval(intervalId); + } + }, [loopRunning]) + + /* const { start, stop } = useInterval({ duration: 3000, startImmediate: false, @@ -79,6 +114,7 @@ const ConfigureWorkflow = (props) => { } }, }); + */ // ONLY when component is being unloaded, run stop() function // This is to prevent the interval from running when the component is not being used @@ -92,16 +128,18 @@ const ConfigureWorkflow = (props) => { */ // Where is this from? - if (workflow === undefined || workflow === null) { + if (workflow === undefined || workflow === null || workflow.id === undefined) { return null; } if (apps === undefined || apps === null) { - return null; + console.log("Apps is undefined or null: ", apps) + return null; } if (appAuthentication === undefined || appAuthentication === null) { - return null; + console.log("App authentication is undefined or null: ", appAuthentication) + return null; } const getApp = (actionId, appId) => { @@ -136,13 +174,19 @@ const ConfigureWorkflow = (props) => { if (firstLoad.length === 0 || firstLoad !== workflow.id) { if (apps === undefined || apps === null || apps.length === 0) { console.log("No apps loaded: ", apps); - setConfigureWorkflowModalOpen(false); + + if (setConfigureWorkflowModalOpen !== undefined) { + 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, @@ -154,34 +198,36 @@ const ConfigureWorkflow = (props) => { auth_done: false, action_ids: [], action: action, - update_version: action.app_version, + update_version: action.app_version, app: {}, - steps: [], - show_steps: false, + steps: [], + show_steps: false, } - //console.log("Action: ", key, keyval) + if (action.app_name.toLowerCase().endsWith("_api")) { + action.app_name = action.app_name.slice(0, -4) + } - 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) + // ID match OR name match + version match + //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))))) + // + // without version match + const newappname = action.app_name.toLowerCase().replaceAll(" ", "_") + const app = apps.find((app) => app.id === action.app_id || app.name.toLowerCase().replaceAll(" ", "_") === newappname) 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" - } + 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, - }) + 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 @@ -199,20 +245,19 @@ const ConfigureWorkflow = (props) => { } } - newaction.steps.push({ - "title": "Authenticate app", - "type": "authenticate", - "required": true, - }) + 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) - - } + console.log("Should verify authentication ID ", action.authentication_id) + } newaction.app = app; } @@ -276,80 +321,78 @@ const ConfigureWorkflow = (props) => { } 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); - } + for (let [key,keyval] in Object.entries(workflow.workflow_variables)) { + const variable = workflow.workflow_variables[key]; - variable.index = 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; + 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) + if (trigger.trigger_type === "WEBHOOK") { + console.log("Found webhook: ", trigger) - // Adding webhook to start of it - if (foundindex >= 0) { - const tmpsteps = newactions[foundindex].steps - newactions[foundindex].steps = [ - { - "title": "Configure Webhook", - "type": "webhook", - "required": true, - } - ] + 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) - for (let [subkey,subkeyval] in Object.entries(tmpsteps)) { - newactions[foundindex].steps.push(tmpsteps[subkey]) - } - - newactions[foundindex].show_steps = true + // Adding webhook to start of it + if (foundindex >= 0) { + const tmpsteps = newactions[foundindex].steps + newactions[foundindex].steps = [ + { + "title": "Configure Webhook", + "type": "webhook", + "required": true, + } + ] - console.log("CHANGED ACTION: ", newactions[foundindex]) - //console.log("Index: ", newactions[foundindex]) + for (let [subkey,subkeyval] in Object.entries(tmpsteps)) { + newactions[foundindex].steps.push(tmpsteps[subkey]) + } + + newactions[foundindex].show_steps = true - continue - } - } + console.log("CHANGED ACTION: ", newactions[foundindex]) + //console.log("Index: ", newactions[foundindex]) + + continue + } + } + } + + if (trigger.status === "running") { + continue; + } + + if ( + trigger.trigger_type === "SUBFLOW" || + trigger.trigger_type === "USERINPUT" + ) { + continue; + } + + requiredTriggers.push(trigger); } + } - if (trigger.status === "running") { - continue; - } + if (requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0 && setConfigureWorkflowModalOpen !== undefined) { + setConfigureWorkflowModalOpen(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); + setRequiredTriggers(requiredTriggers); + setRequiredVariables(requiredVariables); + setRequiredActions(newactions); } if (appAuthentication !== undefined && previousAuth !== undefined && appAuthentication.length !== previousAuth.length) { @@ -381,7 +424,7 @@ const ConfigureWorkflow = (props) => { const { trigger } = props return ( - + { {trigger.status !== "running" ? "Start" : "Running"} ) : null} - {/* - - - ) - }} - fullWidth - color="primary" - type={"text"} - placeholder={`New value for ${trigger.name}`} - onChange={(event) => { - console.log("NEW VALUE ON INDEX", trigger.value) - }} - onBlur={(event) => { - //workflow.variables[variable.index] = event.target.value - }} - /> - } - style={{}} - /> - */} ); }; @@ -488,7 +498,7 @@ const ConfigureWorkflow = (props) => { //Name: {variable.name} - {variable.value}. return ( - + @@ -589,88 +599,312 @@ const ConfigureWorkflow = (props) => { }; + const AppSectionSelfcontained = (props) => { + const { action } = props; + + const [opened, setOpened] = useState(false); + const [filled, setFilled] = useState(false); + const [submitted, setSubmitted] = useState(false); + const [finalized, setFinalized] = useState(false); + + const [authFields, setAuthFields] = useState([]) + const [sensitiveFields, setSensitiveFields] = useState([]) + + if (authFields.length === 0 && opened === true) { + // Loop through fields of the action + + var newfields = [] + const params = action.action.parameters + + var sensitiveIndexes = [] + var index = 0 + for (let key in params) { + const param = params[key] + + if (param.configuration === true) { + if (param.name.toLowerCase().includes("key") || param.name.toLowerCase().includes("token") || param.name.toLowerCase().includes("password")) { + sensitiveIndexes.push(index) + } + + newfields.push({ + "key": param.name, + "example": param.example === undefined ? "" : param.example, + "value": param.name === "url" ? param.example : "", + }) + + index += 1 + } + } + + if (newfields.length > 0) { + setSensitiveFields(sensitiveIndexes) + setAuthFields(newfields) + } + } + + + const submitLocalAuth = (app, fields) => { + const appAuthData = { + active: true, + app: app, + fields: fields, + label: "Authentication for " + app.name, + usage: [{"workflow_id": workflow.id}], + auto_distribute: true, + } + + + 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!"); + } + + setSubmitted(false) + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to set app auth: " + responseJson.reason); + } else { + toast("App auth set for app " + app.name.replace("_", " ")); + setFinalized(true) + setOpened(false) + } + }) + .catch((error) => { + setSubmitted(false) + //toast(error.toString()); + console.log("New auth error: ", error.toString()); + }); + } + + var parsedName = action.app_name.replaceAll("_", " "); + if (action.app_name.toLowerCase().endsWith("_api")) { + parsedName = parsedName.substring(0, parsedName.length - 4); + } + + // Remove _basic at the end if it exists + if (parsedName.toLowerCase().endsWith("_basic")) { + parsedName = parsedName.substring(0, parsedName.length - 6); + } + + parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " "); + + return ( + +
+
{ + setOpened(!opened); + }} + > +
+ + {!opened ? : } + + {parsedName} + + {finalized ? "Authenticated" : `Configure ${parsedName}`} + +
+ {filled ? + + : null} +
+ {opened ? +
+ {authFields.map((field, index) => { + var parsedName = field.key + // Remove _basic at the end if it exists + if (parsedName.toLowerCase().endsWith("_basic")) { + parsedName = parsedName.substring(0, parsedName.length - 6); + } + + parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " "); + + return ( +
+ + {parsedName} + + { + event.preventDefault(); + authFields[index].value = event.target.value; + setAuthFields(authFields); + + var allFilled = true; + authFields.forEach((field) => { + if (field.value.length === 0) { + allFilled = false; + } else { + //console.log("Field is not filled: "+field.key) + } + }) + + if (allFilled) { + console.log("Should test the fields, and submit them") + setFilled(true); + } else { + if (filled) { + setFilled(false); + } + } + }} + + endAdornment={ + // Show item that can show field value if password + //field.name.toLowerCase().includes("key") || field.name.toLowerCase().includes("token") || field.name.toLowerCase().includes("password") ? + field.key.toLowerCase().includes("key") || field.key.toLowerCase().includes("token") || field.key.toLowerCase().includes("password") ? + + { + setSensitiveFields(sensitiveFields.filter((item) => item !== index)) + }} + onMouseDown={(event) => { + event.preventDefault(); + }} + > + + + + : null + } + fullWidth + color="primary" + type={sensitiveFields.includes(index) ? "password" : "text"} + placeholder={field.example ? field.example : `Enter your ${field.key}`} + data-lpignore="true" + dataLPIgnore="true" + autocomplete="off" + /> +
+ ) + })} + +
+ : null} +
+
+ ) + } const AppSection = (props) => { const { action } = props; + var parsedName = action.app_name.replaceAll("_", " "); + if (action.app_name.toLowerCase().endsWith("_api")) { + parsedName = parsedName.substring(0, parsedName.length - 4); + } + return ( - {/* - - - {action.app_name} - - - - */} {action.must_authenticate ? - + if (setAuthenticationModalOpen !== undefined) { + setAuthenticationModalOpen(true); + } + }} + > + {action.app_name} + + {action.auth_done ? "Authenticated" : `Authenticate ${action.app_name.replaceAll("_", " ")}`} + + : null} {action.update_version !== action.app_version ? -
: null} @@ -923,10 +1159,10 @@ const ConfigureWorkflow = (props) => {
{clicked === true ? data.steps.map((step, index) => { - var finished = false + var filled = false if (step.type === "activate") { if (data.activation_done === true) { - finished = true + filled = true if (index === activeStep && firstRun === true) { setActiveStep(activeStep+1) @@ -941,10 +1177,10 @@ const ConfigureWorkflow = (props) => { if (step.type === "authenticate") { console.log("AUTH STEP: ", step) if (data.must_authenticate === true ) { - finished = false + filled = false } else { if (data.activation_done === true && data.auth_done === true) { - finished = true + filled = true if (firstRun) { setFinishCount(finishCount+1) @@ -963,7 +1199,7 @@ const ConfigureWorkflow = (props) => { if (exec.execution_argument !== undefined && exec.execution_argument !== null && exec.execution_argument.length > 0 && exec.execution_source === "webhook") { //console.log("Done: ", exec) - finished = true + filled = true if (index === activeStep && firstRun === true) { setActiveStep(activeStep+1) @@ -991,7 +1227,7 @@ const ConfigureWorkflow = (props) => { } return ( - + ) }) : null} @@ -1002,36 +1238,48 @@ const ConfigureWorkflow = (props) => { const topColor = "#f86a3e, #fc3922" return (
-
-
-
- {workflow.name} - - The following configuration makes the workflow ready immediately. + {setConfigureWorkflowModalOpen !== undefined ? +
+ : null} +
+ + + {setConfigureWorkflowModalOpen !== undefined ? + {workflow.name} + : null + } + + + Please configure the following apps for automatic startup of automation: {requiredActions.length > 0 ? ( - - Required Actions - + {setConfigureWorkflowModalOpen !== undefined ? + + Required Actions + + : null} - + {requiredActions.map((data, index) => { return ( -
- {data.steps !== undefined && data.steps !== null && data.show_steps === true ? - - : - - } -
- ) +
+ {data.steps !== undefined && data.steps !== null && data.show_steps === true && setConfigureWorkflowModalOpen !== undefined ? + + : + setConfigureWorkflowModalOpen !== undefined ? + + : + + } +
+ ) })}
) : null} - {requiredVariables.length > 0 ? ( + {setConfigureWorkflowModalOpen !== undefined && requiredVariables.length > 0 ? ( Variables @@ -1044,7 +1292,7 @@ const ConfigureWorkflow = (props) => { ) : null} - {requiredTriggers.length > 0 && showTriggers !== false ? ( + {setConfigureWorkflowModalOpen !== undefined && requiredTriggers.length > 0 && showTriggers !== false ? ( Triggers @@ -1056,51 +1304,52 @@ const ConfigureWorkflow = (props) => { ) : null} - -
- {showFinalizeAnimation ? - finalize workflow animation { - console.log("Img loaded.") + {setConfigureWorkflowModalOpen !== undefined ? +
+ {showFinalizeAnimation ? + finalize workflow animation { + console.log("Img loaded.") + setTimeout(() => { + console.log("Img closing.") + setConfigureWorkflowModalOpen(false); + }, 1250) + + }}/> + : + + {/* + + */} + - */} - - - } -
-
+ } else { + } + }, 1000) + }} + > + Finalize + + + } +
+ : null} +
); }; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 9971ff3d..81dacd85 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -5,6 +5,7 @@ import { MuiChipsInput } from "mui-chips-input"; import UsecaseSearch from "../components/UsecaseSearch.jsx" import WorkflowGrid from "../components/WorkflowGrid.jsx" import dayjs from 'dayjs'; +import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx"; import { Badge, @@ -57,7 +58,7 @@ import { } from "@mui/icons-material"; const EditWorkflow = (props) => { - const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, } = props + const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, } = props const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove @@ -143,7 +144,9 @@ const EditWorkflow = (props) => { return null } - const newWorkflow = isEditing === true ? false : true + const newWorkflow = isEditing === true ? false : true + const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) + console.log("PRIO: ", priority) var upload = ""; var total_count = 0 @@ -161,6 +164,7 @@ const EditWorkflow = (props) => { maxWidth: isMobile ? "90%" : 650, minHeight: 400, paddingTop: 25, + paddingLeft: 50, //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, }, @@ -170,7 +174,7 @@ const EditWorkflow = (props) => {
- + {newWorkflow ? "New" : "Editing"} workflow {newWorkflow === true ? null : @@ -193,7 +197,7 @@ const EditWorkflow = (props) => {
}
- + Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more {showUpload === true ? @@ -480,7 +484,7 @@ const EditWorkflow = (props) => { - + + {newWorkflow === true ? + + + Relevant Workflows + - {newWorkflow === true && name.length > 5 ? + {priority === null || priority === undefined ? null : +
+ 2 ? priority.description.split("&")[0] : ""} + img1={priority.description.split("&").length > 2 ? priority.description.split("&")[1] : ""} + + dstapp={priority.description.split("&").length > 3 ? priority.description.split("&")[2] : ""} + img2={priority.description.split("&").length > 3 ? priority.description.split("&")[3] : ""} + title={priority.name} + description={priority.description.split("&").length > 4 ? priority.description.split("&")[4] : ""} + + apps={apps} + /> +
+ } + +
+ : null} + + {newWorkflow === true && name.length > 2 ?
{ + const { userdata, globalUrl } = props + const [activeUsecases, setActiveUsecases] = useState(0); + const [modalOpen, setModalOpen] = React.useState(false); + const [suggestedUsecases, setSuggestedUsecases] = useState([]) + const [usecasesSet, setUsecasesSet] = useState(false) + const [apps, setApps] = useState([]) + const sizing = 475 + + let navigate = useNavigate(); + + const imagestyle = { + height: 40, + borderRadius: 40, + //border: "2px solid rgba(255,255,255,0.3)", + } + + const loadApps = () => { + fetch(`${globalUrl}/api/v1/apps`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + return response.json(); + }) + .then((responseJson) => { + if (responseJson === null) { + console.log("null-response from server") + const pretend_apps = [{ + "name": "TBD", + "app_name": "TBD", + "app_version": "TBD", + "description": "TBD", + "version": "TBD", + "large_image": "", + }] + + setApps(pretend_apps) + return + } + + if (responseJson.success === false) { + console.log("error loading apps: ", responseJson) + return + } + + setApps(responseJson); + }) + .catch((error) => { + console.log("App loading error: " + error.toString()); + }) + } + + // Find priorities in userdata.priorities and check if the item.type === "usecase" + // If so, set the item.isActive to true + if (usecasesSet === false && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0 && suggestedUsecases.length === 0) { + + var tmpUsecases = [] + for (let i = 0; i < userdata.priorities.length; i++) { + if (userdata.priorities[i].type !== "usecase" || userdata.priorities[i].active === false) { + continue + } + + tmpUsecases.push(userdata.priorities[i]) + } + + setSuggestedUsecases(tmpUsecases) + setUsecasesSet(true) + loadApps() + } + + const modalView = ( + // console.log("key:", dataValue.key), + //console.log("value:",dataValue.value), + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + +
+ + Sign Up +
+ + Setup +
+ + Explore +
+ + + Here’s a recommended workflow: + + {/*
+
+
+ + { + slidePrev() + }} + > + + + +
+ +
+ + { + slideNext() + }} + > + + + +
+
+
*/} + + + + +
+ ); + + + return ( +
+ {modalView} + + Start using workflows + + + Based on what you selected here’s our recommendations! + + +
+
+
+ + {suggestedUsecases.length === 0 && usecasesSet ? + + All Workflows are already added for your current apps! + + : + suggestedUsecases.map((priority, index) => { + + const srcapp = priority.description.split("&")[0] + var image1 = priority.description.split("&")[1] + var image2 = "" + var dstapp = "" + if (priority.description.split("&").length > 3) { + dstapp = priority.description.split("&")[2] + image2 = priority.description.split("&")[3] + } + + const name = priority.name.replace("Suggested Usecase: ", "") + + var description = "" + if (priority.description.split("&").length > 4) { + description = priority.description[4] + } + + // FIXME: Should have a proper description + description = "" + + return ( + + ) + })} + +
+
+ + + +
+ + + Explore usecases + + +
+
+
+
+
+ ) +} +export default ExploreWorkflow diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 5f66af70..d3137674 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -212,7 +212,8 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho const notificationWidth = 300 const imagesize = 22; - const boxColor = "#86c142"; + const boxColor = "#86c142"; + const NotificationItem = (props) => { const {data} = props @@ -263,7 +264,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho {data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ? - {data.title} + {data.title} ({data.amount}) : @@ -277,7 +278,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho : null } - + {data.description} {/*data.tags !== undefined && data.tags !== null && data.tags.length > 0 ? diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index d963ec0b..c069499f 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -2278,30 +2278,30 @@ const ParsedAction = (props) => { handleItemClick([innerdata]); }} > - - { - //console.log("HOVER: ", pathdata); - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - - {innerdata.name} - - + + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + + {innerdata.name} + + {parsedPaths.map((pathdata, index) => { // FIXME: Should be recursive in here // diff --git a/frontend/src/components/Priority.jsx b/frontend/src/components/Priority.jsx index 5e1c1943..e66784ab 100644 --- a/frontend/src/components/Priority.jsx +++ b/frontend/src/components/Priority.jsx @@ -69,7 +69,7 @@ const Priority = (props) => { return ( -
+
{priority.type === "usecase" || priority.type == "apps" ? : null} diff --git a/frontend/src/components/ShuffleCodeEditor.jsx b/frontend/src/components/ShuffleCodeEditor.jsx index fda866b2..5fb9b152 100644 --- a/frontend/src/components/ShuffleCodeEditor.jsx +++ b/frontend/src/components/ShuffleCodeEditor.jsx @@ -26,22 +26,21 @@ import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx"; import { FullscreenExit as FullscreenExitIcon, Extension as ExtensionIcon, - Apps as AppsIcon, - FavoriteBorder as FavoriteBorderIcon, - Schedule as ScheduleIcon, - FormatListNumbered as FormatListNumberedIcon, + Apps as AppsIcon, + FavoriteBorder as FavoriteBorderIcon, + Schedule as ScheduleIcon, + FormatListNumbered as FormatListNumberedIcon, SquareFoot as SquareFootIcon, - Circle as CircleIcon, - Add as AddIcon, + Circle as CircleIcon, + Add as AddIcon, PlayArrow as PlayArrowIcon, -} from '@mui/icons-material'; - -import { AutoFixHigh as AutoFixHighIcon, + Close as CloseIcon, CompressOutlined, QrCodeScannerOutlined, } from '@mui/icons-material'; + import { validateJson } from "../views/Workflows.jsx"; import ReactJson from "react-json-view"; import PaperComponent from "../components/PaperComponent.jsx"; @@ -823,10 +822,6 @@ const CodeEditor = (props) => { } const executeSingleAction = (inputdata) => { - //if (serverside === true) { - // return - //} - if (validation === true) { inputdata = JSON.stringify(inputdata) } @@ -834,7 +829,10 @@ const CodeEditor = (props) => { // Shuffle Tools 1.2.0 (in most cases?) const appid = toolsAppId !== undefined && toolsAppId !== null && toolsAppId.length > 0 ? toolsAppId : "3e2bdf9d5069fe3f4746c29d68785a6a" - const actiondata = {"description":"Repeats the call parameter","id":"","name":"repeat_back_to_me","label":"","node_type":"","environment":"","sharing":false,"private_id":"","public_id":"","app_id": appid,"tags":null,"authentication":[],"tested":false,"parameters":[{"description":"The message to repeat","id":"","name":"call","example":"REPEATING: Hello world","value":inputdata,"multiline":true,"options":null,"action_field":"","variant":"STATIC_VALUE","required":true,"configuration":false,"tags":null,"schema":{"type":"string"},"skip_multicheck":false,"value_replace":null,"unique_toggled":false,"autocompleted":false}],"execution_variable":{"description":"","id":"","name":"","value":""},"returns":{"description":"","example":"","id":"","schema":{"type":"string"}},"authentication_id":"","example":"","auth_not_required":false,"source_workflow":"","run_magic_output":false,"run_magic_input":false,"execution_delay":0,"app_name":"Shuffle Tools","app_version":"1.2.0","selectedAuthentication":{}} + const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : "repeat_back_to_me" + const params = actionname === "execute_python" ? [{"name": "code", "value":inputdata}] : [{"name":"call", "value": inputdata}] + + const actiondata = {"description":"Repeats the call parameter","id":"","name":actionname,"label":"","node_type":"","environment":"","sharing":false,"private_id":"","public_id":"","app_id": appid,"tags":null,"authentication":[],"tested":false,"parameters": params, "execution_variable":{"description":"","id":"","name":"","value":""},"returns":{"description":"","example":"","id":"","schema":{"type":"string"}},"authentication_id":"","example":"","auth_not_required":false,"source_workflow":"","run_magic_output":false,"run_magic_input":false,"execution_delay":0,"app_name":"Shuffle Tools","app_version":"1.2.0","selectedAuthentication":{}} setExecutionResult({ "valid": false, @@ -924,6 +922,20 @@ const CodeEditor = (props) => { }, }} > + { + setExpansionModalOpen(false) + }} + > + +
{ isFileEditor ? @@ -1555,7 +1567,7 @@ const CodeEditor = (props) => { { executeSingleAction(expOutput) }}> - + {executing ? : } diff --git a/frontend/src/components/WelcomeForm2.jsx b/frontend/src/components/WelcomeForm2.jsx index 83d43e2b..7cc8f052 100644 --- a/frontend/src/components/WelcomeForm2.jsx +++ b/frontend/src/components/WelcomeForm2.jsx @@ -1,78 +1,106 @@ import React, { useState, useEffect } from "react"; -import ReactGA from 'react-ga4'; -import Checkbox from '@mui/material/Checkbox'; +import ReactGA from "react-ga4"; +import Checkbox from "@mui/material/Checkbox"; -import AliceCarousel from 'react-alice-carousel'; -import 'react-alice-carousel/lib/alice-carousel.css'; +import AliceCarousel from "react-alice-carousel"; +import "react-alice-carousel/lib/alice-carousel.css"; -import SearchIcon from '@mui/icons-material/Search'; -import EmailIcon from '@mui/icons-material/Email'; -import NewReleasesIcon from '@mui/icons-material/NewReleases'; -import ExtensionIcon from '@mui/icons-material/Extension'; -import LightbulbIcon from '@mui/icons-material/Lightbulb'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; -import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; - -import theme from '../theme.jsx'; +import SearchIcon from "@mui/icons-material/Search"; +import EmailIcon from "@mui/icons-material/Email"; +import NewReleasesIcon from "@mui/icons-material/NewReleases"; +import ExtensionIcon from "@mui/icons-material/Extension"; +import LightbulbIcon from "@mui/icons-material/Lightbulb"; +import TrendingFlatIcon from "@mui/icons-material/TrendingFlat"; +import theme from "../theme.jsx"; +import CheckBoxSharpIcon from "@mui/icons-material/CheckBoxSharp"; +import AppSearch from "../components/Appsearch.jsx"; +import CloseIcon from "@mui/icons-material/Close"; import { - Button, - Collapse, - IconButton, - FormGroup, - FormControl, - InputLabel, - FormLabel, - FormControlLabel, - Select, - MenuItem, - Grid, - Paper, - Typography, - TextField, - Zoom, - List, - ListItem, - ListItemText, - Divider, - Tooltip, - Chip, - ButtonGroup, + Button, + Collapse, + IconButton, + FormGroup, + FormControl, + InputLabel, + FormLabel, + FormControlLabel, + Select, + MenuItem, + Grid, + Paper, + Typography, + TextField, + Zoom, + List, + ListItem, + ListItemText, + Divider, + Tooltip, + Chip, + ButtonGroup, } from "@mui/material"; -//import { useAlert +//import { useAlert import { useNavigate, Link } from "react-router-dom"; -import WorkflowSearch from '../components/Workflowsearch.jsx'; -import AuthenticationItem from '../components/AuthenticationItem.jsx'; -import WorkflowPaper from "../components/WorkflowPaper.jsx" -import UsecaseSearch from "../components/UsecaseSearch.jsx" - +import WorkflowSearch from "../components/Workflowsearch.jsx"; +import AuthenticationItem from "../components/AuthenticationItem.jsx"; +import WorkflowPaper from "../components/WorkflowPaper.jsx"; +import UsecaseSearch from "../components/UsecaseSearch.jsx"; +import ExploreWorkflow from "../components/ExploreWorkflow.jsx"; const responsive = { - 0: { items: 1 }, + 0: { items: 1 }, +}; + +const imagestyle = { + height: 40, + borderRadius: 40, + //border: "2px solid rgba(255,255,255,0.3)", }; const WelcomeForm = (props) => { - const { userdata, globalUrl, discoveryWrapper, setDiscoveryWrapper, appFramework, getFramework, activeStep, setActiveStep, steps, skipped, setSkipped, getApps, apps, handleSetSearch, usecaseButtons, defaultSearch, setDefaultSearch, selectionOpen, setSelectionOpen, } = props + const { + userdata, + globalUrl, + discoveryWrapper, + setDiscoveryWrapper, + appFramework, + getFramework, + activeStep, + setActiveStep, + steps, + skipped, + setSkipped, + getApps, + apps, + handleSetSearch, + usecaseButtons, + defaultSearch, + setDefaultSearch, + selectionOpen, + setSelectionOpen, + } = props; + const [isActive, setIsActive] = useState(0); - const [usecaseItems, setUsecaseItems] = useState([ - { - "search": "Phishing", - "usecase_search": undefined, - }, - { - "search": "Enrichment", - "usecase_search": undefined, - }, - { - "search": "Enrichment", - "usecase_search": "SIEM alert enrichment", - }, - { - "search": "Build your own", - "usecase_search": undefined, - }]) - - /* + const [usecaseItems, setUsecaseItems] = useState([ + { + search: "Phishing", + usecase_search: undefined, + }, + { + search: "Enrichment", + usecase_search: undefined, + }, + { + search: "Enrichment", + usecase_search: "SIEM alert enrichment", + }, + { + search: "Build your own", + usecase_search: undefined, + }, + ]); + /*
{ const [orgType, setOrgType] = React.useState("") const [finishedApps, setFinishedApps] = React.useState([]) const [authentication, setAuthentication] = React.useState([]); - const [newSelectedApp, setNewSelectedApp] = React.useState({}) - const [thumbIndex, setThumbIndex] = useState(0); + const [newSelectedApp, setNewSelectedApp] = React.useState({}) + const [thumbIndex, setThumbIndex] = useState(0); const [thumbAnimation, setThumbAnimation] = useState(false); const [clickdiff, setclickdiff] = useState(0); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; //const alert = useAlert(); let navigate = useNavigate(); - const onNodeSelect = (label) => { - if (setDiscoveryWrapper !== undefined) { - setDiscoveryWrapper( - {"id": label} - ) - } + const iconStyles = { + color: "rgba(255, 255, 255, 1)", + }; - if (isCloud) { - ReactGA.event({ - category: "welcome", - action: `click_${label}`, - label: "", - }) - } - - setSelectionOpen(true) - setDefaultSearch(label) + const onNodeSelect = (label) => { + if (setDiscoveryWrapper !== undefined) { + setDiscoveryWrapper({ id: label }); } - useEffect(() => { - if (userdata.id === undefined) { - return - } - - if (userdata.name !== undefined && userdata.name !== null && userdata.name.length > 0) { - setName(userdata.name) - } - - if (userdata.active_org !== undefined && userdata.active_org.name !== undefined && userdata.active_org.name !== null && userdata.active_org.name.length > 0) { - setOrgName(userdata.active_org.name) - } - }, [userdata]) - - useEffect(() => { - if (discoveryWrapper === undefined || discoveryWrapper.id === undefined) { - setDefaultSearch("") - var newfinishedApps = finishedApps - newfinishedApps.push(defaultSearch) - setFinishedApps(finishedApps) - } - }, [discoveryWrapper]) - - useEffect(() => { - if ( - window.location.search !== undefined && - window.location.search !== null - ) { - const urlSearchParams = new URLSearchParams(window.location.search); - const params = Object.fromEntries(urlSearchParams.entries()); - const foundTab = params["tab"]; - if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) { - if (foundTab === 3 || foundTab === "3") { - //console.log("Set search!") - } - } else { - //navigate(`/welcome?tab=1`) - } - - const foundTemplate = params["workflow_template"]; - if (foundTemplate !== null && foundTemplate !== undefined) { - console.log("Found workflow template: ", foundTemplate) - - var sourceapp = undefined - var destinationapp = undefined - var action = undefined - const srcapp = params["source_app"]; - if (srcapp !== null && srcapp !== undefined) { - sourceapp = srcapp - } - - const dstapp = params["dest_app"]; - if (dstapp !== null && dstapp !== undefined) { - destinationapp = dstapp - } - - const act = params["action"]; - if (act !== null && act !== undefined) { - action = act - } - - //defaultSearch={foundTemplate} - // - usecaseItems[0] = { - "search": "enrichment", - "usecase_search": foundTemplate, - "sourceapp": sourceapp, - "destinationapp": destinationapp, - "autotry": action === "try", - } - - console.log("Adding: ", usecaseItems[0]) - - setUsecaseItems(usecaseItems) - } - } - }, []) - - const isStepOptional = step => { - return step === 1 + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: `click_${label}`, + label: "", + }); } - const sendUserUpdate = (name, role, userId) => { - const data = { - "tutorial": "welcome", - "firstname": name, - "company_role": role, - "user_id": userId, - } + setSelectionOpen(true); + setDefaultSearch(label); + }; - const url = `${globalUrl}/api/v1/users/updateuser` - 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) { - console.log("Update user success") - //toast("Failed updating org: ", responseJson.reason); - } else { - console.log("Update success!") - //toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - console.log("Update err: ", error.toString()) - //toast("Err: " + error.toString()); - }); - } - - const sendOrgUpdate = (orgname, company_type, orgId, priority) => { - var data = { - org_id: orgId, - }; - - if (orgname.length > 0) { - data.name = orgname - } - - if (company_type.length > 0) { - data.company_type = company_type - } - - if (priority.length > 0) { - data.priority = priority - } - - 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((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - console.log("Update of org failed") - //toast("Failed updating org: ", responseJson.reason); - } else { - //toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - console.log("Update err: ", error.toString()) - //toast("Err: " + error.toString()); - }); - } - - var workflowDelay = -50 - const NewHits = ({ hits }) => { - const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) - var counted = 0 - - const paperAppContainer = { - display: "flex", - flexWrap: "wrap", - alignContent: "space-between", - marginTop: 5, - } - - return ( - - {hits.map((data, index) => { - workflowDelay += 50 - - if (index > 3) { - return null - } - - return ( - - - - - - ) - })} - - ) - } - - const isStepSkipped = step => { - return skipped.has(step) + useEffect(() => { + if (userdata.id === undefined) { + return; } - const handleNext = () => { - setDefaultSearch("") + if ( + userdata.name !== undefined && + userdata.name !== null && + userdata.name.length > 0 + ) { + setName(userdata.name); + } - if (activeStep === 0) { - console.log("Should send basic information about org (fetch)") - setclickdiff(240) - navigate(`/welcome?tab=2`) - - if (isCloud) { - ReactGA.event({ - category: "welcome", - action: "click_page_one_next", - label: "", - }) - } - - if (userdata.active_org !== undefined && userdata.active_org.id !== undefined && userdata.active_org.id !== null && userdata.active_org.id.length > 0) { - sendOrgUpdate(orgName, orgType, userdata.active_org.id, "") - } + if ( + userdata.active_org !== undefined && + userdata.active_org.name !== undefined && + userdata.active_org.name !== null && + userdata.active_org.name.length > 0 + ) { + setOrgName(userdata.active_org.name); + } + }, [userdata]); - if (userdata.id !== undefined && userdata.id !== null && userdata.id.length > 0) { - sendUserUpdate(name, role, userdata.id) - } + useEffect(() => { + if (discoveryWrapper === undefined || discoveryWrapper.id === undefined) { + setDefaultSearch(""); + var newfinishedApps = finishedApps; + newfinishedApps.push(defaultSearch); + setFinishedApps(finishedApps); + } + }, [discoveryWrapper]); - } else if (activeStep === 1) { - console.log("Should send secondary info about apps and other things") - setDiscoveryWrapper({}) - - navigate(`/welcome?tab=3`) - //handleSetSearch("Enrichment", "2. Enrich") - handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) - getApps() + useEffect(() => { + if ( + window.location.search !== undefined && + window.location.search !== null + ) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["tab"]; + if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) { + if (foundTab === 3 || foundTab === "3") { + //console.log("Set search!") + } + } else { + //navigate(`/welcome?tab=1`) + } - // Make sure it's up to date - if (getFramework !== undefined) { - getFramework() - } - } else if (activeStep === 2) { - console.log("Should send third page with workflows activated and the like") - } + const foundTemplate = params["workflow_template"]; + if (foundTemplate !== null && foundTemplate !== undefined) { + console.log("Found workflow template: ", foundTemplate); - - let newSkipped = skipped; - if (isStepSkipped(activeStep)) { - newSkipped = new Set(newSkipped.values()); - newSkipped.delete(activeStep); + var sourceapp = undefined; + var destinationapp = undefined; + var action = undefined; + const srcapp = params["source_app"]; + if (srcapp !== null && srcapp !== undefined) { + sourceapp = srcapp; } - setActiveStep(prevActiveStep => prevActiveStep + 1); - setSkipped(newSkipped); + const dstapp = params["dest_app"]; + if (dstapp !== null && dstapp !== undefined) { + destinationapp = dstapp; + } + + const act = params["action"]; + if (act !== null && act !== undefined) { + action = act; + } + + //defaultSearch={foundTemplate} + // + usecaseItems[0] = { + search: "enrichment", + usecase_search: foundTemplate, + sourceapp: sourceapp, + destinationapp: destinationapp, + autotry: action === "try", + }; + + console.log("Adding: ", usecaseItems[0]); + + setUsecaseItems(usecaseItems); + } } + }, []); - const handleBack = () => { - setActiveStep(prevActiveStep => prevActiveStep - 1); + const isStepOptional = (step) => { + return step === 1; + }; - if (activeStep === 2) { - setDiscoveryWrapper({}) - - if (getFramework !== undefined) { - getFramework() - } - navigate("/welcome?tab=2") - } else if (activeStep === 1) { - navigate("/welcome?tab=1") - } + const sendUserUpdate = (name, role, userId) => { + const data = { + tutorial: "welcome", + firstname: name, + company_role: role, + user_id: userId, }; - const handleSkip = () => { - setclickdiff(240) - if (!isStepOptional(activeStep)) { - throw new Error("You can't skip a step that isn't optional."); - } - setActiveStep(prevActiveStep => prevActiveStep + 1); - setSkipped(prevSkipped => { - const newSkipped = new Set(prevSkipped.values()); - newSkipped.add(activeStep); - return newSkipped; + const url = `${globalUrl}/api/v1/users/updateuser`; + 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) { + console.log("Update user success"); + //toast("Failed updating org: ", responseJson.reason); + } else { + console.log("Update success!"); + //toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + console.log("Update err: ", error.toString()); + //toast("Err: " + error.toString()); + }); + }; + + const sendOrgUpdate = (orgname, company_type, orgId, priority) => { + var data = { + org_id: orgId, + }; + + if (orgname.length > 0) { + data.name = orgname; + } + + if (company_type.length > 0) { + data.company_type = company_type; + } + + if (priority.length > 0) { + data.priority = priority; + } + + 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((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + console.log("Update of org failed"); + //toast("Failed updating org: ", responseJson.reason); + } else { + //toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + console.log("Update err: ", error.toString()); + //toast("Err: " + error.toString()); + }); + }; + + var workflowDelay = -50; + const NewHits = ({ hits }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + var counted = 0; + + const paperAppContainer = { + display: "flex", + flexWrap: "wrap", + alignContent: "space-between", + marginTop: 5, + }; + + return ( + + {hits.map((data, index) => { + workflowDelay += 50; + + if (index > 3) { + return null; + } + + return ( + + + + + + ); + })} + + ); + }; + + const isStepSkipped = (step) => { + return skipped.has(step); + }; + + const handleNext = () => { + setDefaultSearch(""); + + if (activeStep === 0) { + console.log("Should send basic information about org (fetch)"); + setclickdiff(240); + navigate(`/welcome?tab=2`); + + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: "click_page_one_next", + label: "", }); - }; + } - const handleReset = () => { - setActiveStep(0); - }; + if ( + userdata.active_org !== undefined && + userdata.active_org.id !== undefined && + userdata.active_org.id !== null && + userdata.active_org.id.length > 0 + ) { + sendOrgUpdate(orgName, orgType, userdata.active_org.id, ""); + } - useEffect(() => { - console.log("Selected app changed (effect)") - }, [newSelectedApp]) + if ( + userdata.id !== undefined && + userdata.id !== null && + userdata.id.length > 0 + ) { + sendUserUpdate(name, role, userdata.id); + } + } else if (activeStep === 1) { + console.log("Should send secondary info about apps and other things"); + setDiscoveryWrapper({}); - //const buttonWidth = 145 - const buttonWidth = 450 - const buttonMargin = 10 - const sizing = 475 - const buttonStyle = { - flex: 1, - width: "100%", - padding: 25, - margin: buttonMargin, - fontSize: 18, - } + navigate(`/welcome?tab=3`); + //handleSetSearch("Enrichment", "2. Enrich") + handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase); + getApps(); - const slideNext = () => { - if (!thumbAnimation && thumbIndex < usecaseItems.length - 1) { - //handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) - setThumbIndex(thumbIndex + 1); - } else if (!thumbAnimation && thumbIndex === usecaseItems.length - 1) { - setThumbIndex(0) - } - }; + // Make sure it's up to date + if (getFramework !== undefined) { + getFramework(); + } + } else if (activeStep === 2) { + console.log( + "Should send third page with workflows activated and the like" + ); + } - const slidePrev = () => { - if (!thumbAnimation && thumbIndex > 0) { - setThumbIndex(thumbIndex - 1); - } else if (!thumbAnimation && thumbIndex === 0) { - setThumbIndex(usecaseItems.length-1) - } - }; + let newSkipped = skipped; + if (isStepSkipped(activeStep)) { + newSkipped = new Set(newSkipped.values()); + newSkipped.delete(activeStep); + } - const newButtonStyle = { - padding: 22, - flex: 1, - margin: buttonMargin, - minWidth: buttonWidth, - maxWidth: buttonWidth, - } + setActiveStep((prevActiveStep) => prevActiveStep + 1); + setSkipped(newSkipped); + }; + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); - const formattedCarousel = appFramework === undefined || appFramework === null ? [] : usecaseItems.map((item, index) => { - return ( -
- -
- ) - }) + if (activeStep === 2) { + setDiscoveryWrapper({}); - const getStepContent = (step) => { - switch (step) { - case 0: - return ( - - - {/*isCloud ? null : + if (getFramework !== undefined) { + getFramework(); + } + navigate("/welcome?tab=2"); + } else if (activeStep === 1) { + navigate("/welcome?tab=1"); + } + }; + + const handleSkip = () => { + setclickdiff(240); + if (!isStepOptional(activeStep)) { + throw new Error("You can't skip a step that isn't optional."); + } + setActiveStep((prevActiveStep) => prevActiveStep + 1); + setSkipped((prevSkipped) => { + const newSkipped = new Set(prevSkipped.values()); + newSkipped.add(activeStep); + return newSkipped; + }); + }; + + const handleReset = () => { + setActiveStep(0); + }; + + useEffect(() => { + console.log("Selected app changed (effect)"); + }, [newSelectedApp]); + + //const buttonWidth = 145 + const buttonWidth = 450; + const buttonMargin = 10; + const sizing = 475; + const bottomButtonStyle = { + borderRadius: 200, + height: 51, + width: 464, + fontSize: 16, + // background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)", + background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)", + padding: "16px 24px", + // top: 20, + margin: "auto", + itemAlign: "center", + // marginLeft: "65px", + }; + + const buttonStyle = { + flex: 1, + width: 224, + padding: 25, + margin: buttonMargin, + color : "var(--White-text, #F1F1F1)", + fontWeight: 400, + fontSize: 16, + background: "rgba(33, 33, 33, 1)", + borderColor: "rgba(33, 33, 33, 1)", + borderRadius: 8, + }; + + const slideNext = () => { + if (!thumbAnimation && thumbIndex < usecaseItems.length - 1) { + //handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) + setThumbIndex(thumbIndex + 1); + } else if (!thumbAnimation && thumbIndex === usecaseItems.length - 1) { + setThumbIndex(0); + } + }; + + const slidePrev = () => { + if (!thumbAnimation && thumbIndex > 0) { + setThumbIndex(thumbIndex - 1); + } else if (!thumbAnimation && thumbIndex === 0) { + setThumbIndex(usecaseItems.length - 1); + } + }; + + const newButtonStyle = { + padding: 22, + flex: 1, + margin: buttonMargin, + minWidth: buttonWidth, + maxWidth: buttonWidth, + }; + + const formattedCarousel = + appFramework === undefined || appFramework === null + ? [] + : usecaseItems.map((item, index) => { + return ( +
+ +
+ ); + }); + + const getStepContent = (step) => { + switch (step) { + case 0: + return ( + + + {/*isCloud ? null : This data will be used within the product and NOT be shared unless cloud synchronization is configured. */} - - In order to understand how we best can help you find relevant Usecases, please provide the information below. This is optional, but highly encouraged. - - - { - setName(e.target.value) - }} - /> - - - { - setOrgName(e.target.value) - }} - /> - - - - Your Role - - - - - - Company Type - - - - - - ) - case 1: - return ( - -
- + + In order to understand how we best can help you find relevant + Usecases, please provide the information below. This is + optional, but highly encouraged. + + + { + setName(e.target.value); + }} + /> + + + { + setOrgName(e.target.value); + }} + /> + + + + + Your Role + + + + + + + + Company Type + + + + + + + ); + case 1: + return ( + +
+ {selectionOpen ? ( +
+
+ {defaultSearch} + +
+
+ +
+ ) : null} + {/* Apps for each category are shown based on your activity and can be changed by clicking their icon. We will help you connect them later. - - {/*The app framework helps us access and authenticate the most important APIs for you. */} + */} + {/*
+ { + navigate("/welcome") + }}/> + { + navigate("/welcome") + }}> + Back + +
*/} + + Find your apps + + + Select the apps you work with and we will connect the for you. + + {/*The app framework helps us access and authenticate the most important APIs for you. */} - {/* + {/* What is your development experience? @@ -612,39 +821,104 @@ const WelcomeForm = (props) => { */} - - {/*Find your integrations!*/} -
- -
-
- - -
-
- - -
- {/* + + {/*Find your integrations!*/} +
+ +
+
+ + +
+
+ + +
+ {/* What do you want to automate first ? { */}
- {/* - - - What tools do you use? - - - - */} -
- +
+
+ +
+
+ ) case 2: return (
- - These are some of our Workflow templates, used to start new Workflows. Use the right and left buttons to find new Usecases, and click the orange button to build it. -
- - { - slidePrev() - }} - > - - - -
- +
- - { - slideNext() - }} - > - - - -
-
+
+
) @@ -762,36 +977,39 @@ const WelcomeForm = (props) => { } } - const extraHeight = isCloud ? -7 : 0 - return ( -
- {/*selectionOpen ? + + const extraHeight = isCloud ? -7 : 0; + return ( +
+ {/*selectionOpen ? : null*/} -
- {activeStep === steps.length ? ( -
- You Will be Redirected to getting Start Page Wait for 5-sec. - - - -
- ) : ( -
- {getStepContent(activeStep)} -
+
+ {activeStep === steps.length ? ( +
+ You Will be Redirected to getting Start Page Wait for 5-sec. + + + +
+ ) : ( +
+ {getStepContent(activeStep)} + {/*
{activeStep === 2 || activeStep === 1 ?
- {/*isStepOptional(activeStep) && ( + // (commented) isStepOptional(activeStep) && ( - )*/} + ) //commented : null}
- } -
- )} -
-
- ); -} + }*/} +
+ )} +
+
+ ); +}; -export default WelcomeForm +export default WelcomeForm; diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index f99bbc52..589e4f7d 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -154,7 +154,6 @@ const AppGrid = props => { }) .then((responseJson) => { if (responseJson.success !== false) { - console.log("Usecases: ", responseJson) //handleKeysetting(responseJson, workflows) } }) @@ -187,10 +186,10 @@ const AppGrid = props => { }, []) if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) { - console.log("In refinement: ", inputsearch) //setLocalMessage(inputsearch) refine(inputsearch) defaultSearch = inputsearch + return null } else if (onlyResults === true) { // Don't return anything unless refinement works return null @@ -244,9 +243,7 @@ const AppGrid = props => { return (
{onlyResults === true && hits.length > 0 ? - - Relevant Workflows - + null : null} {hits.map((data, index) => { diff --git a/frontend/src/components/WorkflowPaper.jsx b/frontend/src/components/WorkflowPaper.jsx index e07daca6..7ab4c546 100644 --- a/frontend/src/components/WorkflowPaper.jsx +++ b/frontend/src/components/WorkflowPaper.jsx @@ -277,7 +277,7 @@ const WorkflowPaper = (props) => { > {data.tags !== undefined && data.tags !== null ? data.tags.map((tag, index) => { - if (index >= 3) { + if (index >= 2) { return null; } diff --git a/frontend/src/components/WorkflowTemplatePopup.jsx b/frontend/src/components/WorkflowTemplatePopup.jsx new file mode 100644 index 00000000..9db5c484 --- /dev/null +++ b/frontend/src/components/WorkflowTemplatePopup.jsx @@ -0,0 +1,422 @@ +import React, { useState, useEffect } from "react"; + +import { toast } from "react-toastify" +import theme from '../theme.jsx'; +import { + Button, + Typography, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Drawer, + CircularProgress, + IconButton, + Tooltip, +} from "@mui/material"; + +import { + Check as CheckIcon, + TrendingFlat as TrendingFlatIcon, + Close as CloseIcon, +} from '@mui/icons-material'; + +import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx"; +import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; + +const WorkflowTemplatePopup = (props) => { + const { userdata, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps } = props; + + const [isActive, setIsActive] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [workflowLoading, setWorkflowLoading] = useState(false); + const [workflow, setWorkflow] = useState({}); + + const [appAuthentication, setAppAuthentication] = React.useState(undefined); + const imagestyleWrapper = { + height: 40, + width: 40, + borderRadius: 40, + border: "1px solid rgba(255,255,255,0.3)", + overflow: "hidden", + display: "flex", + } + + const imagestyleWrapperDefault = { + height: 40, + width: 40, + borderRadius: 40, + border: "1px solid red", + overflow: "hidden", + display: "flex", + } + + const imagestyle = { + height: 40, + width: 40, + borderRadius: 40, + border: "1px solid rgba(255,255,255,0.3)", + overflow: "hidden", + } + + const imagestyleDefault = { + display: "block", + marginLeft: 9, + marginTop: 10, + } + + if (title === undefined || title === null || title === "") { + console.log("No title for workflow template popup!"); + return null + } + + const getWorkflow = (workflowId) => { + fetch(`${globalUrl}/api/v1/workflows/${workflowId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + console.log("Error in workflow loading for ID ", workflowId) + } else { + setWorkflow(responseJson) + } + }) + .catch((error) => { + console.log("err in framework: ", error.toString()); + setWorkflowLoading(false) + }) + } + + const loadAppAuth = () => { + 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 setting app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to get app auth: " + responseJson.reason); + return + } + + var newauth = []; + for (let authkey in responseJson.data) { + if (responseJson.data[authkey].defined === false) { + continue; + } + + newauth.push(responseJson.data[authkey]); + } + + setAppAuthentication(newauth); + }) + .catch((error) => { + //toast(error.toString()); + console.log("New auth error: ", error.toString()); + }); + } + + const getGeneratedWorkflow = () => { + // POST + // https://shuffler.io/api/v1/workflows/merge + // destination: {app_id: "b9c2feaf99b6309dabaeaa8518c61d3d", app_name: "Servicenow_API", app_version: "",…} + // id: "" + // middle:[] + // name: "Email analysis" + // source:{app_id: "accdaaf2eeba6a6ed43b2efc0112032d", app_name + + setWorkflowLoading(true) + + + // FIXME: Remove hardcoding here after testing, and user srcapp/dstapp + const newsrcapp = srcapp + const newdstapp = dstapp + + const mergedata = { + name: title, + id: "", + source: { + app_name: newsrcapp, + }, + middle: [], + destination: { + app_name: newdstapp, + }, + } + + fetch(globalUrl + "/api/v1/workflows/merge", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(mergedata), + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + setWorkflowLoading(false) + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + console.log("Error in workflow template: ", responseJson.error); + + setErrorMessage("Failed to generate workflow for these tools - the Shuffle team has been notified. Click out of this window to continue. Contact support@shuffler.io for further assistance.") + + setIsActive(true) + //setTimeout(() => { + // setModalOpen(false) + //}, 5000) + } else { + console.log("Success in workflow template: ", responseJson); + setIsActive(true) + if (responseJson.workflow_id === "") { + console.log("Failed to build workflow for these tools. Closing in 3 seconds.") + return + } + + getWorkflow(responseJson.workflow_id) + } + }) + .catch((error) => { + console.log("err in framework: ", error.toString()); + setWorkflowLoading(false) + }) + } + + const isFinished = () => { + // Look for configuration fields being done in the current modal + // 1. Start by finding the modal + const template = document.getElementById("workflow-template") + if (template === null || template == undefined) { + return true + } + + // Find item in template with id app-config + const appconfig = template.getElementsByClassName("app-config") + if (appconfig === null || appconfig == undefined) { + return true + } + + console.log("APPCONFIG: ", appconfig) + + return false + } + + const ModalView = () => { + return ( + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: "black", + color: "white", + minWidth: 700, + maxWidth: 700, + paddingTop: 75, + itemAlign: "center", + }, + }} + > + { + setModalOpen(false); + }} + > + + + + + Configure Workflow + + + Selected Workflow: + +
+ +
+ {workflowLoading ? +
+ Generating the Workflow... + + +
+ : +
+ + {errorMessage !== "" ? errorMessage : ""} + +
+ } + + {errorMessage === "" ? + + : null} +
+
+ ) + } + + var parsedTitle = title + const maxlength = 30 + if (title.length > maxlength) { + parsedTitle = title.substring(0, maxlength) + "..." + } + + parsedTitle = parsedTitle.replaceAll("_", " ") + + const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : "" + + + return ( +
+ +
{ + setIsHovered(true) + }} + onMouseLeave={() => { + setIsHovered(false) + }} + onClick={() => { + if (visualOnly === true) { + console.log("Not showing more than visuals.") + return + } + + //setIsActive(!isActive) + if (errorMessage !== "") { + toast("Already failed to generate workflow for these apps. Please try again later or contact support@shuffler.io.") + } else if (isActive) { + toast("Workflow already generated. Please try another workflow template!") + + // FIXME: Remove these? + loadAppAuth() + setModalOpen(true) + //getGeneratedWorkflow() + } else { + + loadAppAuth() + setModalOpen(true) + getGeneratedWorkflow() + } + }} + > +
+
+ {img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ? + + + + + + : + + } + {img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ? + + + + + + + : + + } +
+
+ + {parsedTitle} + + + {parsedDescription} + +
+
+
+ {isActive === true && errorMessage === "" ? + + : ""} +
+
+
+ ) +} + +export default WorkflowTemplatePopup diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx index e24f7a0a..8d13070c 100644 --- a/frontend/src/theme.jsx +++ b/frontend/src/theme.jsx @@ -18,10 +18,12 @@ const theme = createTheme(adaptV4Theme({ secondary: "rgba(255,255,255,0.7)", }, type: "dark", - inputColor: "#383B40", + inputColor: "rgba(39,41,45,1)", + //inputColor: "#383B40", surfaceColor: "#27292d", platformColor: "#1c1c1d", backgroundColor: "#1a1a1a", + green: "#5cc879", borderRadius: 5, defaultBorder: "1px solid rgba(255,255,255,0.3)", jsonTheme: "brewer", @@ -70,11 +72,6 @@ const theme = createTheme(adaptV4Theme({ }, }, overrides: { - MuiPaper: { - root: { - backgroundColor: "#1c1c1d", - }, - }, MuiMenu: { list: { backgroundColor: "#27292d", diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index c8269a52..866f9360 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1692,15 +1692,24 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user const userId = user.id; const data = { user_id: userId }; - fetch(globalUrl + "/api/v1/generateapikey", { + console.log(user, userdata) + + var fetchdata = { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(data), credentials: "include", - }) + } + + if (userId === userdata.id) { + fetchdata.method = "GET" + } else { + fetchdata.body = JSON.stringify(data) + } + + fetch(globalUrl + "/api/v1/generateapikey", fetchdata) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); @@ -1889,8 +1898,11 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user open={selectedUserModalOpen} onClose={() => { setSelectedUserModalOpen(false); - setImage2FA(""); - setSecret2FA(""); + + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); }} PaperProps={{ style: { @@ -2156,7 +2168,7 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user /> - Billing + Billing (Beta) /> 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 @@ -3299,7 +3308,6 @@ Let me know if you're interested, or set up a call here: https://drift.me/${user ) : null; const run2FASetup = (data) => { - console.log("2fa: ", data, show2faSetup); if (!show2faSetup) { get2faCode(data.id); } else { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index e8ccdde7..1dd3eb87 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -580,15 +580,39 @@ const AngularWorkflow = (defaultprops) => { const cytoscapeWidth = isMobile ? bodyWidth - leftBarSize : bodyWidth - leftBarSize - 25 const [elements, setElements] = useState([]); + const [loopRunning, setLoopRunning] = useState(false) + + const stop = () => { + setLoopRunning(false) + } + + const start = () => { + setLoopRunning(true) + } + + useEffect(() => { + console.log("In useeffect for loopRunning: ", loopRunning) + if (loopRunning) { + const intervalId = setInterval(() => { + if (!loopRunning) { + clearInterval(intervalId); + } + + fetchUpdates() + }, 3000) + + return () => clearInterval(intervalId); + } + }, [loopRunning]) + // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. - // Set it from 2500 to 6000 to reduce overall load - const { start, stop } = useInterval({ - duration: 3000, - startImmediate: false, - callback: () => { - fetchUpdates(); - }, - }); + //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}`, { @@ -924,7 +948,7 @@ const AngularWorkflow = (defaultprops) => { }; const getWorkflowExecution = (id, execution_id) => { - fetch(`${globalUrl}/api/v1/workflows/${id}/executions`, { + fetch(`${globalUrl}/api/v2/workflows/${id}/executions`, { method: "GET", headers: { "Content-Type": "application/json", @@ -940,11 +964,10 @@ const AngularWorkflow = (defaultprops) => { return response.json(); }) .then((responseJson) => { - if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { - // FIXME: Sort this by time + if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null && responseJson.executions.length > 0) { // - means it's opposite - const newkeys = sortByKey(responseJson, "-started_at"); + const newkeys = sortByKey(responseJson.executions, "-started_at"); setWorkflowExecutions(newkeys); const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; @@ -1021,6 +1044,7 @@ const AngularWorkflow = (defaultprops) => { }, body: JSON.stringify(executionRequest), credentials: "include", + cors: "no-cors", }) .then((response) => { if (response.status !== 200) { @@ -1279,7 +1303,7 @@ const AngularWorkflow = (defaultprops) => { }; const sendStreamRequest = (body) => { - console.log("Stream not activated yet.") + //console.log("Stream not activated yet.") return // Session may be important here huh @@ -15302,6 +15326,10 @@ const AngularWorkflow = (defaultprops) => { overflow: "hidden", }} onMouseOver={() => { + if (cy == undefined || cy == null) { + return + } + var currentnode = cy.getElementById(data.action.id); if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) { currentnode.addClass("shuffle-hover-highlight"); @@ -15314,6 +15342,10 @@ const AngularWorkflow = (defaultprops) => { //) }} onMouseOut={() => { + if (cy == undefined || cy == null) { + return + } + var currentnode = cy.getElementById(data.action.id); if (currentnode.length !== 0) { currentnode.removeClass("shuffle-hover-highlight"); diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5871c03a..28b02f52 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -228,15 +228,16 @@ const parseCurl = (s) => { // Basically CRUD for each category + special export const appCategories = [ { - "name": "Communication", + "name": "Communication", "color": "#FFC107", "icon": "communication", "action_labels": ["List Messages", "Send Message", "Get Message", "Search messages", "List Attachments", "Get Attachment", "Get Contact"], - }, { + }, + { "name": "SIEM", "color": "#FFC107", "icon": "siem", - "action_labels": ["Search", "List Alerts", "Close Alert", "Get Alert", "Create detection", "Add to lookup list",], + "action_labels": ["Search", "List Alerts", "Close Alert", "Get Alert", "Create detection", "Add to lookup list", "Isolate endpoint",], }, { "name": "Eradication", "color": "#FFC107", diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 218f9e39..d5e88e8c 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -287,6 +287,7 @@ const Apps = (props) => { const [selectedAction, setSelectedAction] = React.useState({}); const [searchBackend, setSearchBackend] = React.useState(false); const [searchableApps, setSearchableApps] = React.useState([]); + const [publishModalOpen, setPublishModalOpen] = React.useState(false); const [openApi, setOpenApi] = React.useState(""); const [openApiData, setOpenApiData] = React.useState(""); @@ -441,6 +442,7 @@ const Apps = (props) => { if (privateapps.length > 0) { if (selectedApp.id === undefined || selectedApp.id === null) { setSelectedApp(privateapps[0]); + setSharingConfiguration(privateapps[0].sharing === true ? "public" : "you") } if ( @@ -646,6 +648,7 @@ const Apps = (props) => { if (selectedApp.id !== data.id) { data.name = newAppname; setSelectedApp(data); + setSharingConfiguration(data.sharing === true ? "public" : "you") if ( data.actions !== undefined && @@ -658,7 +661,7 @@ const Apps = (props) => { } if (data.sharing) { - setSharingConfiguration(isCloud ? "public" : "everyone"); + setSharingConfiguration("public"); } } }} @@ -999,11 +1002,11 @@ const Apps = (props) => { ); }; - const userRoles = ["you", isCloud ? "public" : "everyone"]; + const userRoles = ["you", "public"]; - // Admin in org or creator of app - // FIXME: Missing check for if same creator account - const canEditApp = userdata !== undefined && (userdata.admin === "true" || userdata.id === selectedApp.owner || selectedApp.owner === "" || (userdata.admin === "true" && userdata.active_org.id === selectedApp.reference_org)) || !selectedApp.generated + // Admin in org or creator of app + // FIXME: Missing check for if same creator account + const canEditApp = userdata !== undefined && (userdata.admin === "true" || userdata.id === selectedApp.owner || selectedApp.owner === "" || (userdata.admin === "true" && userdata.active_org.id === selectedApp.reference_org)) || !selectedApp.generated //fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"), var baseInfo = @@ -1054,6 +1057,7 @@ const Apps = (props) => { console.log("New version: ", newversion); selectedApp.app_version = selectedApp.app_version; setSelectedApp(selectedApp); + setSharingConfiguration(selectedApp.sharing === true ? "public" : "you") if (newversion !== undefined && newversion !== null) { getApp(newversion.id, true); @@ -1156,17 +1160,22 @@ const Apps = (props) => {