diff --git a/backend/app_gen/openapi/baseline/requirements.txt b/backend/app_gen/openapi/baseline/requirements.txt index dfad3eb9..c0d2f96f 100755 --- a/backend/app_gen/openapi/baseline/requirements.txt +++ b/backend/app_gen/openapi/baseline/requirements.txt @@ -1,3 +1,11 @@ # No extra requirements needed -requests -urllib3 +requests==2.32.3 +urllib3==2.3.0 +liquidpy==0.8.2 +MarkupSafe==3.0.2 +flask[async]==3.1.0 +python-dateutil==2.9.0.post0 +PyJWT==2.10.1 +cryptography==44.0.2 +shufflepy==0.1.0 +shuffle-sdk==0.0.25 diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 9f0c72e2..b7ee7e6d 100755 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -211,7 +211,7 @@ func fixTags(tags []string) []string { func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string, downloadIfFail bool) error { ctx := context.Background() client, err := client.NewEnvClient() - defer client.Close() + defer client.Close() if err != nil { log.Printf("Unable to create docker client: %s", err) return err @@ -473,73 +473,84 @@ func buildImage(tags []string, dockerfileLocation string) error { } } } - } else { - - ctx := context.Background() - client, err := client.NewEnvClient() - defer client.Close() - if err != nil { - log.Printf("Unable to create docker client: %s", err) - return err - } - - log.Printf("[INFO] Docker Tags: %s", tags) - dockerfileSplit := strings.Split(dockerfileLocation, "/") - - // Create a buffer - buf := new(bytes.Buffer) - tw := tar.NewWriter(buf) - defer tw.Close() - baseDir := strings.Join(dockerfileSplit[0:len(dockerfileSplit)-1], "/") - - // Builds the entire folder into buf - err = getParsedTar(tw, baseDir, "") - if err != nil { - log.Printf("Tar issue: %s", err) - } - - dockerFileTarReader := bytes.NewReader(buf.Bytes()) - buildOptions := types.ImageBuildOptions{ - Remove: true, - Tags: tags, - BuildArgs: map[string]*string{}, - } - //NetworkMode: "host", - - httpProxy := os.Getenv("HTTP_PROXY") - if len(httpProxy) > 0 { - buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy - } - httpsProxy := os.Getenv("HTTPS_PROXY") - if len(httpProxy) > 0 { - buildOptions.BuildArgs["https_proxy"] = &httpsProxy - } - - // Build the actual image - imageBuildResponse, err := client.ImageBuild( - ctx, - dockerFileTarReader, - buildOptions, - ) - - if err != nil { - return err - } - - // Read the STDOUT from the build process - defer imageBuildResponse.Body.Close() - buildBuf := new(strings.Builder) - _, err = io.Copy(buildBuf, imageBuildResponse.Body) - if err != nil { - return err - } else { - if strings.Contains(buildBuf.String(), "errorDetail") { - log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) - return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ","))) - } - } + return nil } + + ctx := context.Background() + client, err := client.NewEnvClient() + defer client.Close() + if err != nil { + log.Printf("Unable to create docker client: %s", err) + return err + } + + log.Printf("[INFO] Docker Tags: %s", tags) + dockerfileSplit := strings.Split(dockerfileLocation, "/") + + // Create a buffer + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + defer tw.Close() + baseDir := strings.Join(dockerfileSplit[0:len(dockerfileSplit)-1], "/") + + // Builds the entire folder into buf + err = getParsedTar(tw, baseDir, "") + if err != nil { + log.Printf("[ERROR] Tar issue during app build: %s", err) + } + + dockerFileTarReader := bytes.NewReader(buf.Bytes()) + buildOptions := types.ImageBuildOptions{ + Remove: true, + Tags: tags, + BuildArgs: map[string]*string{}, + } + //NetworkMode: "host", + + httpProxy := os.Getenv("HTTP_PROXY") + if len(httpProxy) > 0 { + buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy + } + httpsProxy := os.Getenv("HTTPS_PROXY") + if len(httpProxy) > 0 { + buildOptions.BuildArgs["https_proxy"] = &httpsProxy + } + + // Print the actual file content from dockerFileTarReader + /* + data, err := ioutil.ReadAll(dockerFileTarReader) + if err != nil { + log.Printf("[ERROR] Failed reading Dockerfile TAR reader: %s", err) + } else { + log.Printf("[DEBUG] Dockerfile TAR reader content: %s", string(data)) + } + */ + + // Build the actual image + imageBuildResponse, err := client.ImageBuild( + ctx, + dockerFileTarReader, + buildOptions, + ) + + if err != nil { + return err + } + + // Read the STDOUT from the build process + defer imageBuildResponse.Body.Close() + buildBuf := new(strings.Builder) + _, err = io.Copy(buildBuf, imageBuildResponse.Body) + if err != nil { + return err + } else { + if strings.Contains(buildBuf.String(), "errorDetail") { + log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) + return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ","))) + } + } + return nil } @@ -671,7 +682,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) { resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "No image name"}`))) return - + } log.Printf("[INFO] Trying to download image: '%s'. Appname: '%s'. BaseAppname: '%s', Split2: %s", version.Name, appname, baseAppname, appnameSplit2) @@ -870,7 +881,7 @@ func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user type tmpapp struct { Success bool `json:"success"` OpenAPI string `json:"openapi"` - App string `json:"app"` + App string `json:"app"` } app := tmpapp{} diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index c630b9ff..7f30ce38 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -22,7 +22,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.58 + github.com/shuffle/shuffle-shared v0.8.72 golang.org/x/crypto v0.37.0 google.golang.org/api v0.228.0 google.golang.org/grpc v1.71.1 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index e4b4e8e0..154bcdde 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -341,8 +341,8 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fc github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shuffle/shuffle-shared v0.8.58 h1:QzCKtQjuaozb+xyLkw6IjwqCNVWzpuJJnxkpXkLCP9M= -github.com/shuffle/shuffle-shared v0.8.58/go.mod h1:OLAwH/Ym4941Jn5DF1oZaq6iBpmjG2SNrTZ9Xqck5So= +github.com/shuffle/shuffle-shared v0.8.72 h1:HVOsRt83/1k9P+8q1FAxXnDKyROoDAFa1A3MnoRJYb0= +github.com/shuffle/shuffle-shared v0.8.72/go.mod h1:OLAwH/Ym4941Jn5DF1oZaq6iBpmjG2SNrTZ9Xqck5So= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 2e1cf238..e6f08faa 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -11,17 +11,18 @@ import ( "crypto/md5" "strconv" + "os" + "io" + "log" + "fmt" + "errors" + "net/url" + "os/exec" + "net/http" + "io/ioutil" + "math/rand" "encoding/hex" "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "os/exec" "net/http/httptest" "strings" @@ -35,9 +36,9 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/storage/memory" gitProxy "github.com/go-git/go-git/v5/plumbing/transport" http2 "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/storage/memory" // Random xj "github.com/basgys/goxml2json" @@ -61,6 +62,7 @@ var registryName = "registry.hub.docker.com" var runningEnvironment = "onprem" var syncUrl = "https://shuffler.io" +//var syncUrl = "http://localhost:5002" type retStruct struct { Success bool `json:"success"` @@ -447,7 +449,7 @@ func checkGitProxy(cloneOptions *git.CloneOptions) *git.CloneOptions { func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) error { // Returns false if there is an issue // Use this for register - err := shuffle.CheckPasswordStrength(password) + err := shuffle.CheckPasswordStrength(username, password) if err != nil { log.Printf("[WARNING] Bad password strength: %s", err) return err @@ -460,8 +462,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) } ctx := context.Background() - //users, err := FindUser(ctx context.Context, username string) ([]User, error) { - users, err := shuffle.FindUser(ctx, strings.ToLower(strings.TrimSpace(username))) if err != nil && len(users) == 0 { log.Printf("[WARNING] Failed getting user %s: %s", username, err) @@ -486,7 +486,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) newUser.Active = true newUser.Orgs = []string{org.Id} - // FIXME - Remove this later if role == "admin" { newUser.Role = "admin" newUser.Roles = []string{"admin"} @@ -1852,6 +1851,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // return //} + log.Printf("[DEBUG] HOOKS: webhook callback: %s", request.URL.String()) + if request.Method != "POST" { request.Method = "POST" } @@ -1863,6 +1864,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { path := strings.Split(request.URL.String(), "/") if len(path) < 4 { + log.Printf("[DEBUG] HOOKS: Invalid webhook path: %s", request.URL.String()) resp.WriteHeader(403) resp.Write([]byte(`{"success": false}`)) return @@ -1878,7 +1880,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { if location[1] == "api" { if len(location) <= 4 { log.Printf("[INFO] Couldn't handle location. Too short in webhook: %d", len(location)) - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } @@ -1895,6 +1897,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } } + log.Printf("[DEBUG] HOOKS: Pre user agent check") + // Find user agent header userAgent := request.Header.Get("User-Agent") if strings.Contains(strings.ToLower(userAgent), "microsoftpreview") || strings.Contains(strings.ToLower(userAgent), "googlebot") { @@ -1917,8 +1921,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //log.Printf("HookID: %s", hookId) hook, err := shuffle.GetHook(ctx, hookId) if err != nil { - log.Printf("[WARNING] Failed getting hook %s (callback): %s", hookId, err) - resp.WriteHeader(401) + log.Printf("[WARNING] HOOKS: Failed getting hook %s (callback): %s", hookId, err) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } @@ -1930,21 +1934,21 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //resp.WriteHeader(200) //resp.Write([]byte(`{"success": true}`)) if hook.Status == "stopped" { - log.Printf("[WARNING] Not running %s because hook status is stopped", hook.Id) - resp.WriteHeader(401) + log.Printf("[WARNING] HOOKS: Not running %s because hook status is stopped", hook.Id) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Is it running?"}`))) return } if len(hook.Workflows) == 0 { - log.Printf("[DEBUG] Not running because hook isn't connected to any workflows") - resp.WriteHeader(401) + log.Printf("[DEBUG] HOOKS: Not running because hook isn't connected to any workflows") + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflows are defined"}`))) return } if hook.Environment == "cloud" { - log.Printf("[DEBUG] This should trigger in the cloud. Duplicate action allowed onprem.") + log.Printf("[DEBUG] HOOKS: This should trigger in the cloud. Duplicate action allowed onprem.") } // Check auth @@ -1960,7 +1964,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadAll(request.Body) if err != nil { - log.Printf("[DEBUG] Body data error: %s", err) + log.Printf("[DEBUG] HOOKS: data read error: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -2001,7 +2005,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { b, err := json.Marshal(newBody) if err != nil { - log.Printf("[ERROR] Failed newBody marshaling for webhook: %s", err) + log.Printf("[ERROR] HOOKS: Failed newBody marshaling for webhook: %s", err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return @@ -2017,7 +2021,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } if len(hook.Start) == 0 { - log.Printf("[WARNING] No start node for hook %s - running with workflow default.", hook.Id) + log.Printf("[ERROR] HOOKS: No start node for hook %s - running with workflow default.", hook.Id) //bodyWrapper = string(parsedBody) } @@ -2029,7 +2033,6 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest, hook.OrgId) - if err == nil { if hook.Version == "v2" { timeout := 15 @@ -2064,6 +2067,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } else { resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s"}`, workflowExecution.ExecutionId))) } + return } @@ -2071,6 +2075,10 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) } + log.Printf("[ERROR] HOOKS: END OF FUNCTION FOR '%s'. IF this is reached, something went wrong.", hook.Id) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Failed to run workflow. Check logs."}`)) + } func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) { @@ -3087,7 +3095,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s return } - if user.Id == app.Owner || (user.Role == "admin" && user.ActiveOrg.Id == app.ReferenceOrg) || shuffle.ArrayContains(app.Contributors, user.Id) { + if user.Id == app.Owner || (user.Role == "admin" && user.ActiveOrg.Id == app.ReferenceOrg) || shuffle.ArrayContains(app.Contributors, user.Id) { log.Printf("[DEBUG] Editing app %s with user %s (%s) in org %s", test.Id, user.Username, user.Id, user.ActiveOrg.Id) } else { log.Printf("[WARNING] Wrong user (%s) for app %s when verifying swagger", user.Username, app.Name) @@ -3375,7 +3383,6 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s } } - log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID) if len(user.Id) > 0 { resp.WriteHeader(200) @@ -3799,30 +3806,56 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { } } - if org.SyncConfig.WorkflowBackup { - workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") - if err != nil { - log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) - } else { - backupJob.Workflows = workflows - } + // Check if it's 1/20 times (600 seconds - 10 min on average) + // Only problem: May take time to sync the first time, which is annoying + shouldBackupData := false + randomNumber := rand.Intn(20) + if randomNumber == 0 { + shouldBackupData = true } - 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 + // Just to prevent it from spamming large outbound requests + if shouldBackupData { + if org.SyncConfig.WorkflowBackup { + workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") + if err != nil { + log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) + } else { + backupJob.Workflows = workflows + } } - } - 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 + if org.SyncConfig.AppBackup && len(org.Users) > 0 { + foundUser.ActiveOrg.Id = org.Id + apps, err := shuffle.GetPrioritizedApps(ctx, foundUser) + if err != nil { + log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err) + } else { + parsedApps := []shuffle.WorkflowApp{} + for _, app := range apps { + if len(app.Actions) == 0 { + continue + } + + if !app.Generated { + continue + } + + parsedApps = append(parsedApps, app) + } + + backupJob.Apps = parsedApps + } + } + + // Send stats once every 10 times or so..? + // For now, just send every time + 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) @@ -3859,6 +3892,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { //log.Printf("[ERROR] Failed cloud sync job controller run for '%s': %s", respBody, err) return err } + return nil } @@ -3996,6 +4030,8 @@ func runInitEs(ctx context.Context) { time.Sleep(30 * time.Second) } + // FIXME: This should ONLY run on one backend instance + schedules, err := shuffle.GetAllSchedules(ctx, "ALL") if err != nil { log.Printf("[WARNING] Failed getting schedules during service init: %s", err) @@ -4139,7 +4175,7 @@ func runInitEs(ctx context.Context) { } //interval := int(org.SyncConfig.Interval) - interval := 15 + interval := 30 if interval == 0 { log.Printf("[WARNING] Skipping org %s because sync isn't set (0).", org.Id) continue @@ -4241,17 +4277,20 @@ func runInitEs(ctx context.Context) { continue } - if newresp.StatusCode != 200 { - log.Printf("[WARNING] Failed stopping runs in environment %s. Status code: %d", environment, newresp.StatusCode) + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("[ERROR] Failed setting respbody %s for execution stop. Status: %d", err, newresp.StatusCode) continue } - //respBody, err := ioutil.ReadAll(newresp.Body) - //if err != nil { - // log.Printf("[ERROR] Failed setting respbody %s", err) - // continue - //} - //log.Printf("[DEBUG] Successfully ran workflow cleanup request for %s. Body: %s", environment, string(respBody)) + if newresp.StatusCode != 200 { + if !strings.Contains(string(respBody), "is active") { + log.Printf("[WARNING] Failed stopping runs in environment %s. Status code: %d. Body: %s", environment, newresp.StatusCode, string(respBody)) + } + + continue + } url = fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/rerun", backendPort, environment) req, err = http.NewRequest( @@ -4377,7 +4416,7 @@ func runInitEs(ctx context.Context) { } if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" { - healthcheckInterval := 60 + healthcheckInterval := 60 log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats, and dashboard on /health. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) job := func() { // Prepare a fake http.responsewriter @@ -4669,7 +4708,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // If you want to disable cloud sync, see previous section. if org.CloudSync { log.Printf("[WARNING] Org %s is already syncing. Skip", org.Id) - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Your org is already syncing. Nothing to set up."}`))) return } @@ -4746,6 +4785,9 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { org.SyncConfig = shuffle.SyncConfig{ Apikey: responseData.SessionKey, Interval: responseData.IntervalSeconds, + + WorkflowBackup: true, + AppBackup: true, } interval := int(responseData.IntervalSeconds) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 8357de3e..4329e4af 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1905,8 +1905,8 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { err = shuffle.SetSchedule(ctx, newSchedule) if err != nil { - log.Printf("Failed setting cloud schedule: %s", err) - resp.WriteHeader(401) + log.Printf("[ERROR] Failed setting cloud schedule: %s", err) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } @@ -1941,17 +1941,22 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - real error message lol if err != nil { - log.Printf("Failed creating schedule: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. Try cron */15 * * * *"}`))) + log.Printf("[ERROR] Failed creating schedule: %s", err) + + resp.WriteHeader(400) + if schedule.Environment == "cloud" { + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. For cloud schedules, try cron */15 * * * *"}`))) + } else { + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. For onprem schedules, try 60 for 60 seconds"}`))) + } return } //workflow.Schedules = append(workflow.Schedules, schedule) err = shuffle.SetWorkflow(ctx, *workflow, workflow.ID) if err != nil { - log.Printf("Failed setting workflow for schedule: %s", err) - resp.WriteHeader(401) + log.Printf("[ERROR] Failed setting workflow for schedule: %s", err) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7a8c31da..7f110c98 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -468,7 +468,7 @@ const App = (message, props) => { { const ComponentToRender = selectedItemData.component; const componentProps = selectedItemData.props; - return ; + const updatedProps = { + ...componentProps, + notifications: notifications, + setNotifications: setNotifications, + userdata: userdata, + selectedOrganization: selectedOrganization }; + return ; +}; + const defaultImage = "/images/logos/orange_logo.svg" const imageData = selectedOrganization?.image === undefined || selectedOrganization?.image.length === 0 diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index 643aa8dc..8908590f 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -66,7 +66,7 @@ import { Context } from '../context/ContextApi.jsx'; const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ) const AppAuthTab = memo((props) => { @@ -1181,6 +1181,47 @@ const AppAuthTab = memo((props) => { )} + + { + navigator.clipboard.writeText(data.id); + document.execCommand("copy"); + + toast(data.id + " copied to clipboard"); + }} + > + + + + + + + { diff --git a/frontend/src/components/AppSearch1.jsx b/frontend/src/components/AppSearch1.jsx index 3947f640..449ad925 100644 --- a/frontend/src/components/AppSearch1.jsx +++ b/frontend/src/components/AppSearch1.jsx @@ -13,7 +13,7 @@ import { InputAdornment, Typography, } from '@mui/material'; -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder, diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index d13eb9a1..8fbb7e38 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -20,7 +20,7 @@ import { } from '@mui/material'; import aa from 'search-insights' -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props const { themeMode } = useContext(Context) diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index b0e85c8d..0b9daa6f 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -52,11 +52,13 @@ import { //import { useAlert import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; -import BillingStats from "./BillingStats.jsx"; +import BillingStats from "../components/BillingStats.jsx"; +import LicencePopup from "../components/LicencePopup.jsx"; import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" -import DeleteIcon from '@mui/icons-material/Delete'; + import { Context } from "../context/ContextApi.jsx"; -import LicencePopup from "./LicencePopup.jsx"; + +import DeleteIcon from '@mui/icons-material/Delete'; import { DataGrid } from "@mui/x-data-grid"; const Billing = memo((props) => { @@ -2597,64 +2599,87 @@ const Billing = memo((props) => { Utilization & Stats - {isChildOrg ? ( - - ): ( - <> + setCurrentTab(newValue)} + onChange={(event, newValue) => { + setCurrentTab(-1) + + // Force re-render + setTimeout(() => { + setCurrentTab(newValue) + }, 100); + }} style={{ marginTop: 20 }} TabIndicatorProps={{ style: { - height: 3, - backgroundColor: theme.palette.primary.main, - marginLeft: 12, - marginRight: 12, + height: 3, + backgroundColor: theme.palette.primary.main, + marginLeft: 12, + marginRight: 12, } }} > - - + + + {isCloud ? + + : null} + + - {currentTab === 0 ? ( -
- -
- ): ( - - )} - - )} +
+ {currentTab === 0 ? +
+ +
+ : currentTab === 1 ? +
+ +
+ : + + } +
+
) diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 7b94c67c..5bb09ddb 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -33,8 +33,15 @@ import { import { BarChart, + BarSeries, + Bar, + BarLabel, + GridlineSeries, Gridline, + TooltipArea, + ChartTooltip, + TooltipTemplate, } from 'reaviz'; import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; @@ -45,31 +52,52 @@ const LineChartWrapper = ({keys, inputname, height, width}) => { const inputdata = keys.data === undefined ? keys : keys.data const {themeMode} = useContext(Context) const theme = getTheme(themeMode) - + + return (
- + {inputname} + + } + /> + } gridlines={ } /> } /> +
) } const AppStats = (defaultprops) => { - const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows,clickedFromOrgTab } = defaultprops; + const { + globalUrl, + selectedOrganization, + userdata, + isCloud, + inputWorkflows, + clickedFromOrgTab, + syncStats, + } = defaultprops; const [keys, setKeys] = useState([]) const [searches, setSearches] = useState([]); const [appRuns, setAppruns] = useState(undefined); + const [childOrgsAppRuns, setChildOrgsAppRuns] = useState(undefined); const [appRunCosts, setApprunCosts] = useState(undefined); const [workflowRuns, setWorkflowRuns] = useState(undefined); const [subflowRuns, setSubflowRuns] = useState(undefined); @@ -99,9 +127,6 @@ const AppStats = (defaultprops) => { const getWorkflowStats = async (workflow, startTime, endTime) => { - if (!userdata.support) { - return workflow - } if (workflow.id === undefined || workflow.id === null || workflow.id === "") { return workflow @@ -166,12 +191,8 @@ const AppStats = (defaultprops) => { } const loadWorkflowStats = (foundWorkflows, startTime, endTime) => { - if (!userdata.support) { - return - } - if (foundWorkflows === undefined || foundWorkflows === null || foundWorkflows.length === 0) { - console.log("Not workflows") + setResultLoading(false) return } @@ -180,6 +201,9 @@ const AppStats = (defaultprops) => { const promises = foundWorkflows.slice(0, 50).map(wf => getWorkflowStats(wf, startTime, endTime)); const allData = Promise.all(promises); + if (allData === undefined || allData === null) { + setResultLoading(false) + } allData.then((data) => { var total = 0 @@ -239,15 +263,16 @@ const AppStats = (defaultprops) => { return } - if (statistics["daily_statistics"] === undefined || statistics["daily_statistics"] === null) { + const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" + if (statistics[statKey] === undefined || statistics[statKey] === null) { setFilteredStatistics(statistics) return } // Calculate month to date cost var mtd_cost = 0 - for (let key in statistics["daily_statistics"]) { - const item = statistics["daily_statistics"][key] + for (let key in statistics[statKey]) { + const item = statistics[statKey][key] if (item["date"] === undefined) { continue } @@ -305,8 +330,8 @@ const AppStats = (defaultprops) => { // Check if start time is before the daily statistics["date"] string var newlist = [] - for (let key in statistics["daily_statistics"]) { - const item = statistics["daily_statistics"][key] + for (let key in statistics[statKey]) { + const item = statistics[statKey][key] if (item["date"] === undefined) { continue } @@ -337,7 +362,7 @@ const AppStats = (defaultprops) => { var appexecutions = 0 var estimatedcost = 0 if (newlist.length > 0) { - tmpstats["daily_statistics"] = newlist + tmpstats[statKey] = newlist for (let key in newlist) { const item = newlist[key] @@ -391,7 +416,8 @@ const AppStats = (defaultprops) => { return } - const dailyStats = inputdata.daily_statistics + const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" + const dailyStats = inputdata[statKey] if (dailyStats === undefined || dailyStats === null) { return } @@ -401,6 +427,11 @@ const AppStats = (defaultprops) => { "data": [] } + var childorgappRuns = { + "key": "Child Org App Runs", + "data": [] + } + var workflowRuns = { "key": "Workflow Runs (includes subflows)", "data": [] @@ -442,6 +473,13 @@ const AppStats = (defaultprops) => { }) } + if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) { + childorgappRuns["data"].push({ + key: new Date(item["date"]), + data: inputdata["child_app_executions"] + }) + } + // Check if workflow_executions key in item if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) { workflowRuns["data"].push({ @@ -471,6 +509,15 @@ const AppStats = (defaultprops) => { }) } + if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { + childorgappRuns["data"].push({ + key: new Date(), + data: inputdata["daily_child_app_executions"] + }) + + //setApprunCosts(appcostRuns) + } + if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { workflowRuns["data"].push({ key: new Date(), @@ -485,6 +532,11 @@ const AppStats = (defaultprops) => { }) } + // Only for parent orgs + if (childorgappRuns["data"].length > 0) { + setChildOrgsAppRuns(childorgappRuns) + } + setSubflowRuns(subflowRuns) setWorkflowRuns(workflowRuns) setAppruns(appRuns) @@ -659,44 +711,57 @@ const AppStats = (defaultprops) => { style={{ textDecoration: "none", color: theme.palette.linkColor,}} >Your Organisation Statistics. It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. + +
+ {syncStats !== true ? null : + "PS: You are currently looking at data from your onprem synced org"}
{filteredStatistics !== undefined ?
- - The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. - - }> - - - ${selectedOrganization?.lead_info?.customer === false && selectedOrganization?.lead_info?.pov === false ? - 0 - : - apprunCost - } + + {syncStats == true ? null : + + The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. - - Period Cost - - - + }> + + + ${selectedOrganization?.lead_info?.customer === false && selectedOrganization?.lead_info?.pov === false ? + 0 + : + apprunCost + } + + + + Period Cost + + + + } + + {syncStats === true ? null : App runs in the selected period }> - - - {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} - - - App Runs - - + + + {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} + + + App Runs + + + } + + {syncStats === true ? null : Workflow runs in the selected period @@ -711,20 +776,24 @@ const AppStats = (defaultprops) => { - - Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. - - }> - - - ${monthTotalCost} + } + + {syncStats === true ? null : + + Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. - - Estimated cost - - - + }> + + + ${monthTotalCost} + + + Estimated cost + + + + }
: null}
@@ -895,7 +964,13 @@ const AppStats = (defaultprops) => { {appRuns === undefined ? null : - + + } + + {childOrgsAppRuns === undefined ? + null + : + } {workflowRuns === undefined ? @@ -916,56 +991,58 @@ const AppStats = (defaultprops) => { */} + {syncStats === true ? null : +
+ {resultLoading ? +
+ + Loading usage for selected period (may take a while) + + + +
+ : + { + //setRowsPerPage(newPageSize) + //submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize) + }} + // event for when clicking next page + // Hide page changer + onPageChange={(params) => { + console.log("page params: ", params) + }} + onSelectionModelChange={(newSelection) => { + console.log("newSelection: ", newSelection) + //console.log("newSelection: ", newSelection) + //setSelectedWorkflowExecutionsIndexes(newSelection) + //var found = [] + //for (var i = 0; i < newSelection.length; i++) { + // // Find the workflow in the resultRows + // var selected = resultRows.find((workflow) => { + // return workflow.id === newSelection[i] + // }) -
- {resultLoading ? -
- - Loading usage for selected period (may take a while) - - -
- : - { - //setRowsPerPage(newPageSize) - //submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize) - }} - // event for when clicking next page - // Hide page changer - onPageChange={(params) => { - console.log("page params: ", params) - }} - onSelectionModelChange={(newSelection) => { - console.log("newSelection: ", newSelection) - //console.log("newSelection: ", newSelection) - //setSelectedWorkflowExecutionsIndexes(newSelection) - //var found = [] - //for (var i = 0; i < newSelection.length; i++) { - // // Find the workflow in the resultRows - // var selected = resultRows.find((workflow) => { - // return workflow.id === newSelection[i] - // }) + // if (selected === undefined || selected === null) { + // continue + // } - // if (selected === undefined || selected === null) { - // continue - // } + // found.push(selected) + //} - // found.push(selected) - //} - - //setSelectedWorkflowExecutions(found) - }} - // Track which items are selected - /> - } -
+ //setSelectedWorkflowExecutions(found) + }} + // Track which items are selected + /> + } +
+ }
) diff --git a/frontend/src/components/Branding.jsx b/frontend/src/components/Branding.jsx index 92e434e5..40a76846 100644 --- a/frontend/src/components/Branding.jsx +++ b/frontend/src/components/Branding.jsx @@ -39,7 +39,7 @@ const Branding = (props) => { const theme = getTheme(themeMode, brandColor) const [selectedBrandColor, setSelectedBrandColor] = useState(theme?.palette?.main || "#FF8544") const [selectedBrandName, setSelectedBrandName] = useState(selectedOrganization?.branding?.brand_name || "") - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const [isLoading,setIsLoading] = useState(false); const handleEditOrg = (joinStatus) => { @@ -391,7 +391,7 @@ const Branding = (props) => { - { integrationPartner ? ( + {integrationPartner ? ( <> diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 9cff6231..9c45df62 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -867,7 +867,7 @@ const CacheView = memo((props) => { overflowX: "auto", }}> - {["Key", "Value", "Actions", "Updated", "Distribution"].map((header, index) => ( + {["Key", "Value", "workflow", "Actions", "Updated", "Distribution"].map((header, index) => ( { backgroundColor: theme.palette.platformColor, }} > - {Array(5) + {Array(6) .fill() .map((_, colIndex) => ( { data.value } /> + + +
+ : ( + + + + + + + + + + ) + } + style={{ + display: "table-cell", + overflow: "hidden", + verticalAlign: "middle", + padding: "8px 8px 8px 15px", + maxWidth: 200, + overflowX: "auto", + }} + /> { const [, forceUpdate] = React.useState(); const itemColor = "white"; const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io"; + const { themeMode, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); + useEffect(() => { getSettings(); }, []); + const GridItem = (props) => { const [expanded, setExpanded] = React.useState(false); const [showEdit, setShowEdit] = React.useState(false); const [newValue, setNewValue] = React.useState(-100); - const primary = props.data.primary; + var primary = props.data.primary + + const shownName = props.data.newname !== undefined && props.data.newname !== null && props.data.newname !== primary ? props.data.newname : primary + const secondary = props.data.secondary; const primaryIcon = props.data.icon; const secondaryIcon = props.data.active ? @@ -191,7 +197,7 @@ const CloudSyncTab = (props) => { {isCloud && userdata.support === true ? @@ -477,7 +483,7 @@ const CloudSyncTab = (props) => { } else { toast("Cloud Syncronization successfully set up!"); setOrgSyncResponse( - "Successfully started syncronization. Cloud features you now have access to can be seen below." + "Successfully started syncronization. Cloud/Hybrid features are available below." ); } @@ -535,8 +541,8 @@ const CloudSyncTab = (props) => { Cloud syncronization - What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. - + What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. This will by default back up apps and workflows. + {isCloud ? ( @@ -708,7 +714,7 @@ const CloudSyncTab = (props) => { )} - Features + {isCloud ? "Cloud" : "Hybrid"} Features Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. @@ -717,7 +723,9 @@ const CloudSyncTab = (props) => { {selectedOrganization.sync_features === undefined || selectedOrganization.sync_features === null ? + {[...Array(18)].map((_, i) => ( +
{ } const newkey = key.replaceAll("_", " "); + + // Rewrites to frontend names + var newname = newkey + if (newkey === "app executions") { + newname = "app runs" + } + const griditem = { primary: newkey, secondary: @@ -770,6 +785,8 @@ const CloudSyncTab = (props) => { data_collection: "None", active: item.active, icon: , + + newname: newname, }; return ( diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index 8dea326c..b6f0c734 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -633,7 +633,7 @@ const ConfigureWorkflow = (props) => { if (aa !== undefined) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() diff --git a/frontend/src/components/CreatorGrid.jsx b/frontend/src/components/CreatorGrid.jsx index f3922e3e..96e9c059 100644 --- a/frontend/src/components/CreatorGrid.jsx +++ b/frontend/src/components/CreatorGrid.jsx @@ -37,7 +37,7 @@ import { AvatarGroup, } from "@mui/material" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const CreatorGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx index 7a32001c..d22beaa4 100644 --- a/frontend/src/components/DocsGrid.jsx +++ b/frontend/src/components/DocsGrid.jsx @@ -29,7 +29,7 @@ import { -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const DocsGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 8265f079..a156e4d6 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -188,7 +188,7 @@ const EditWorkflow = (props) => { } const newWorkflow = isEditing === true ? false : true - const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) + const priority = userdata === undefined || userdata === null || userdata.priorities === null || userdata.priorities === undefined ? null : userdata?.priorities?.find(prio => prio.type === "usecase" && prio.active === true) var upload = ""; var total_count = 0 @@ -226,8 +226,8 @@ const EditWorkflow = (props) => {
- - {newWorkflow ? "New" : "Editing"} workflow + + {newWorkflow ? "New" : "Editing"} Workflow {newWorkflow === true ? null : @@ -393,7 +393,7 @@ const EditWorkflow = (props) => {
- +
{ diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 39fce780..d10d4f00 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -403,7 +403,6 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { const handleChangeTheme = (newTheme) => { - toast.info("Changing theme to " + newTheme + " - please wait!"); const data = { "org_id": userdata?.active_org?.id, @@ -449,8 +448,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { }); }; + const handleUpdateTheme = (newTheme) => { - + handleThemeChange(newTheme); + setCurrentSelectedTheme(newTheme) + const data = { "user_id": userdata?.id, "theme": newTheme, @@ -470,14 +472,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { }).then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - toast("Failed updating theme: ", responseJson.reason); - } else { - handleThemeChange(newTheme); - setCurrentSelectedTheme(newTheme); + toast("Failed saving your theme: ", responseJson.reason); } }) ).catch((error) => { - console.log("Error changing theme: ", error); + console.log("Error saving your theme: ", error); }); }; @@ -518,6 +517,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { console.error("Logout error:", error); }); }; + const avatarMenu = ( { } if (userdata?.org_status?.includes("integration_partner")){ handleChangeTheme(newTheme); - }else { + } else { + handleUpdateTheme(newTheme); } }} @@ -646,7 +647,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { - + 0 ? userdata?.active_org?.branding?.documentation_link : "/docs" } target={userdata?.active_org?.branding?.documentation_link?.length > 0 && userdata?.org_status?.includes("integration_partner") ? "_blank" : "_self" } style={hrefStyle}> { handleClose(); @@ -670,7 +671,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { - Version: 2.0.2 + Version: 2.1.0-rc1 @@ -1632,7 +1633,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { + + + + ) : null + + const modalView = notificationWorkflowModal ? ( + { + setNotificationWorkflowModal(false); + }} + > + + +
+ {`Configure ${selectedAppDetails.name} workflow`} +
+
+ + + {console.log("len Selected app details: ", selectedAppDetails)} + {(selectedAppDetails.authentication_data || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false) || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ? + <> + + {true || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ? "No authentication required" : + <> + + Pick an authentication method from the list + + + Available authentications + + } + + + + Provide additional required details: + + { + setTextFieldOneValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + { + setTextFieldValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + + : + <> + 0) ? false : true} + // // setAuthenticationModalOpen={false} + selectedApp={{ ...selectedAppDetails, authentication: selectedAppDetails.auth_config }} + // getAppAuthentication={selectedAppDetails.name} + /> + + } + + + + + +
+
+ ) : null + + + const checkIfAlreadyGenerated = async (appList, workflows) => { // fixxxxxxxxxxxxxxxxxxxxx + + var workflowName = workflows.find(workflow => workflow.id === notificationWorkflow) + if (workflowName) { + workflowName = workflowName.name + } + else { + console.log("no workflow set") + return + } + if (workflowName) { + const parts = workflowName.split(' '); + console.log("parts", parts) + if (parts[0].toString() === "[GENERATED]" && parts.length > 1) { + console.log("parts1", parts[1]) + if ((appList.includes(parts[1]))) { + console.log("workflow already generated") + setGeneatedWorkflow({ "app_name": parts[1] }) + } + } + } + else { + return + } + } + + const renderChips = useCallback(() => { + const appList = Object.values(notificationAppsDetails); + return ( + + + {appList.map((app) => ( + { + console.log(`Clicked ${app.name}`) + console.log("app: ", app) + setSelectedAppDetails(app) + if (app.authentication_data && app.authentication_data.length > 0) { //fixxxxxxxx + console.log("authdata: ", app.authentication_data[0]) + setSelectedAuth(app.authentication_data[app.authentication_data.length - 1].id) + } + setNotificationWorkflowModal(true) + // getAppAuth(app.name) + console.log("selectedAppDEtails", selectedAppDetails) + }} + avatar={{app.name}} + /> + ))} + + + + + + Want access to more templates? + + Set up app authentication + + to unlock additional workflow options. + + + + ); + }, [notificationAppsDetails]) + useEffect(() => { getFramework() @@ -83,7 +1223,7 @@ const Priorities = memo((props) => { setSelectedExecutionId(execution_id) //toast.info("Execution-related notifications are highlighted.") - } + } if (workflow !== null) { setSelectedWorkflow(workflow) @@ -97,7 +1237,7 @@ const Priorities = memo((props) => { return } - if(workflows?.length === 0) { + if (workflows?.length === 0) { getAvailableWorkflows() } @@ -107,7 +1247,7 @@ const Priorities = memo((props) => { }, [selectedOrganization]) if (userdata === undefined || userdata === null) { - return + return } const getFramework = () => { @@ -119,67 +1259,67 @@ const Priorities = memo((props) => { }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for framework!"); - } - - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === false) { - setAppFramework({}) - if (responseJson.reason !== undefined) { - //toast("Failed loading: " + responseJson.reason) - } else { - //toast("Failed to load framework for your org.") + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); } - } else { - setAppFramework(responseJson) - } - }) - .catch((error) => { - console.log("err in framework: ", error.toString()); - }) + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + setAppFramework({}) + if (responseJson.reason !== undefined) { + //toast("Failed loading: " + responseJson.reason) + } else { + //toast("Failed to load framework for your org.") + } + } else { + setAppFramework(responseJson) + } + }) + .catch((error) => { + console.log("err in framework: ", error.toString()); + }) } - const clearNotifications = () => { - // Don't really care about the logout + const clearNotifications = () => { + // Don't really care about the logout - toast("Clearing notifications") - fetch(`${globalUrl}/api/v1/notifications/clear`, { - credentials: "include", - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } + toast("Clearing notifications") + fetch(`${globalUrl}/api/v1/notifications/clear`, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - // Reload the UI - const newNotifications = notifications.map((notification) => { - notification.read = true - return notification - }) + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + // Reload the UI + const newNotifications = notifications.map((notification) => { + notification.read = true + return notification + }) - setNotifications(newNotifications) - setShowRead(true) - } else { - toast("Failed dismissing notifications. Please try again later."); - } - }) - .catch((error) => { - console.log("error in notification dismissal: ", error); - //removeCookie("session_token", {path: "/"}) - }); - }; + setNotifications(newNotifications) + setShowRead(true) + } else { + toast("Failed dismissing notifications. Please try again later."); + } + }) + .catch((error) => { + console.log("error in notification dismissal: ", error); + //removeCookie("session_token", {path: "/"}) + }); + }; const dismissNotification = (alert_id, disabled) => { var notificationurl = `${globalUrl}/api/v1/notifications/${alert_id}/markasread` @@ -189,82 +1329,82 @@ const Priorities = memo((props) => { notificationurl += "?disabled=false" } - fetch(notificationurl , { - credentials: "include", - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } - - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - // Mark current one as read - var newNotifications = notifications.map((notification) => { - if (notification.id === alert_id) { - notification.read = true + fetch(notificationurl, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); } - return notification + return response.json(); }) + .then(function (responseJson) { + if (responseJson.success === true) { + // Mark current one as read + var newNotifications = notifications.map((notification) => { + if (notification.id === alert_id) { + notification.read = true + } + + return notification + }) - if (disabled === true) { - toast("Notification disabled, and will not be shown again.") + if (disabled === true) { + toast("Notification disabled, and will not be shown again.") - newNotifications = newNotifications.map((notification) => { - if (notification.id === alert_id) { - notification.ignored = true + newNotifications = newNotifications.map((notification) => { + if (notification.id === alert_id) { + notification.ignored = true + } + + return notification + }) + + console.log("NEW NOTIFICATIONS: ", newNotifications); + } else if (disabled === false) { + toast("Notification re-enabled successfully") + + newNotifications = newNotifications.map((notification) => { + if (notification.id === alert_id) { + notification.ignored = false + } + + return notification + }) + + } else { + toast("Notification dismissed successfully") } - return notification - }) + //const newNotifications = notifications.filter( + // (data) => data.id !== alert_id + //) - console.log("NEW NOTIFICATIONS: ", newNotifications); - } else if (disabled === false) { - toast("Notification re-enabled successfully") + //console.log("NEW NOTIFICATIONS: ", newNotifications); - newNotifications = newNotifications.map((notification) => { - if (notification.id === alert_id) { - notification.ignored = false + if (setNotifications !== undefined && newNotifications !== undefined) { + setNotifications(newNotifications) } - - return notification - }) - - } else { - toast("Notification dismissed successfully") - } - - //const newNotifications = notifications.filter( - // (data) => data.id !== alert_id - //) - - //console.log("NEW NOTIFICATIONS: ", newNotifications); - - if (setNotifications !== undefined && newNotifications !== undefined) { - setNotifications(newNotifications) - } - } else { - toast("Failed dismissing notification. Please try again later."); - } - }) - .catch((error) => { - console.log("error in notification dismissal: ", error); - //removeCookie("session_token", {path: "/"}) - }) + } else { + toast("Failed dismissing notification. Please try again later."); + } + }) + .catch((error) => { + console.log("error in notification dismissal: ", error); + //removeCookie("session_token", {path: "/"}) + }) } - - const notificationWidth = "100%" + + const notificationWidth = "100%" const imagesize = 22 - const boxColor = "#86c142" + const boxColor = "#86c142" const getAvailableWorkflows = () => { @@ -354,134 +1494,168 @@ const Priorities = memo((props) => { } return ( -
-
-
- - Notification Workflow - - - The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. You can point child org notifications into the parent org notification by choosing it in the list. - - -
+
+
+
+ + Notification Workflow + + + The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. You can point child org notifications into the parent org notification by choosing it in the list. + - {workflows !== undefined && workflows !== null && workflows.length > 0 ? - { - setOpenNotification(true); - }} - onClose={() => { - setOpenNotification(false); - }} - freeSolo - //autoSelect - value={workflows?.find(w => w.id === notificationWorkflow) || null} - classes={{ inputRoot: classes.inputRoot }} - ListboxProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: theme.palette.text.primary, - borderRadius: theme.palette.borderRadius, - }, - }} - getOptionLabel={(option) => { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return "No Workflow Selected"; - } + {modalView} + {/*{testWorkflowModal} */} +
+ {renderChips()} +
- const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={workflows} - fullWidth - style={{ - backgroundColor: theme.palette.textFieldStyle.backgroundColor, - borderRadius: theme.palette.textFieldStyle.borderRadius, - color: theme.palette.textFieldStyle.color, - height: 35, - marginBottom: 40, - }} - onChange={(event, newValue) => { - console.log("Found value: ", newValue) +
- var parsedinput = { target: { value: newValue } } - - // For variables - if (typeof newValue === 'string' && newValue.startsWith("$")) { - parsedinput = { - target: { - value: { - "name": newValue, - "id": newValue, - "actions": [], - "triggers": [], - } - } - } - } - - handleWorkflowSelectionUpdate(parsedinput) - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } - - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose {data.name} - - - } placement="bottom"> - 0 ? + { + setOpenNotification(true); + }} + onClose={() => { + setOpenNotification(false); + }} + freeSolo + //autoSelect + value={workflows?.find(w => w.id === notificationWorkflow) || null} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { backgroundColor: theme.palette.surfaceColor, - color: data.id === workflow.id ? "red" : theme.palette.text.primary, - borderBottom: data.id === "parent" ? "2px solid rgba(255,255,255,0.5)" : null - }} - value={data} - onClick={(e) => { - props.onMouseDown?.(null); - var parsedinput = { target: { value: data } } - handleWorkflowSelectionUpdate(parsedinput) - }} - > - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, - color: theme.palette.textFieldStyle.color, borderRadius: theme.palette.textFieldStyle.borderRadius, + color: theme.palette.textFieldStyle.color, height: 35, - fontSize: 16, - marginTop: "16px" + marginBottom: 40, }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + props.onMouseDown?.(null); + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { borderRadius: 4, }, inputProps: { - ...params.inputProps, style: { height: "100%", boxSizing: "border-box", @@ -499,198 +1672,171 @@ const Priorities = memo((props) => { } }} - // label="Find a notification workflow" - variant="outlined" - placeholder="Select a notification workflow" + style={{ + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, + borderRadius: 4, + height: 35, + fontSize: 16, + marginBottom: 30 + }} + fullWidth={true} + type="name" + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="ID of the workflow to receive notifications" + value={notificationWorkflow} + onChange={(e) => { + setNotificationWorkflow(e.target.value); + }} /> - ); - }} - /> - : - { - setNotificationWorkflow(e.target.value); - }} - /> - } - {/*
+ {/*
{orgSaveButton}
*/} -
+
- {notificationWorkflow === undefined || notificationWorkflow === null || notificationWorkflow.length === 0 ? null : -
- + { + if (notificationWorkflow === "parent") { + toast.error("Can't open parent org's notification workflow from here.") + return + } + + window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank") + }} + > + + +
} - fetch(`${globalUrl}/api/v1/workflows/${notificationWorkflow}/execute`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - "title": "Test Notification", - "description": "This is a test notification to check if the notification workflow is working correctly.", - "org_id": selectedOrganization.id, - "id": uuidv4(), - "reference_url": "/admin?type=test&admin_tab=notifications", - "created_at": Math.floor(new Date().getTime() / 1000), - "updated_at": Math.floor(new Date().getTime() / 1000), + Notifications ({ + notifications?.filter((notification) => showRead === true || notification.read === false).length + }) + + + Notifications help you find potential problems with your workflows and apps.  + + Learn more + + +
+
+ { + setShowRead(!showRead); + }} + />  Show read + {notifications !== undefined && notifications !== null && notifications.length > 1 ? ( + + ) : null} +
+ + + + {clickedFromOrgTab ? null : } + + Suggestions + + Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
These range from simple configurations in Shuffle to Usecases you may have missed.  + + Learn more + +
+
+ { + setShowDismissed(!showDismissed); + }} + />  Show dismissed + {userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ? + + No Suggestions found + + : + userdata.priorities.map((priority, index) => { + if (showDismissed === false && priority.active === false) { + return null + } + + return ( + + ) }) - }) - .then((response) => { - if (response.status === 200) { - toast.success("Test notification sent successfully.") - } else { - toast.error("Failed to send test notification. Please contact support if this persists") - } - }).catch((error) => { - toast.error("Failed to send test notification (2). Please contact support if this persists") - }) - }}> - Send test notification - - { - if (notificationWorkflow === "parent") { - toast.error("Can't open parent org's notification workflow from here.") - return - } - - window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank") - }} - > - - -
- } - - Notifications ({ - notifications?.filter((notification) => showRead === true || notification.read === false).length - }) - - - Notifications help you find potential problems with your workflows and apps.  - - Learn more - - -
-
- { - setShowRead(!showRead); - }} - />  Show read - {notifications !== undefined && notifications !== null && notifications.length > 1 ? ( - - ) : null} -
- - - - {clickedFromOrgTab? null : } - - Suggestions - - Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
These range from simple configurations in Shuffle to Usecases you may have missed.  - - Learn more - -
-
- { - setShowDismissed(!showDismissed); - }} - />  Show dismissed - {userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ? - - No Suggestions found - - : - userdata.priorities.map((priority, index) => { - if (showDismissed === false && priority.active === false) { - return null } - - return ( - - ) - }) - } -
-
+
+
) }) @@ -699,15 +1845,15 @@ export default Priorities; const NotificationItem = memo((props) => { - const {data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification} = props + const { data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification } = props var image = ""; var orgName = ""; var orgId = ""; const { themeMode, brandColor } = useContext(Context); - const theme = getTheme(themeMode, brandColor); - - var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) + const theme = getTheme(themeMode, brandColor); + + var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) if (!highlighted && highlightKMS) { if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) { @@ -719,45 +1865,45 @@ const NotificationItem = memo((props) => { } if (userdata.orgs !== undefined) { - const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); - if (foundOrg !== undefined && foundOrg !== null) { - //position: "absolute", bottom: 5, right: -5, - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - marginLeft: - data.creator_org !== undefined && data.creator_org.length > 0 - ? 20 - : 0, - borderRadius: 10, - border: - foundOrg.id === userdata.active_org.id - ? `3px solid ${boxColor}` - : null, - cursor: "pointer", - marginRight: 10, - }; + const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); + if (foundOrg !== undefined && foundOrg !== null) { + //position: "absolute", bottom: 5, right: -5, + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginLeft: + data.creator_org !== undefined && data.creator_org.length > 0 + ? 20 + : 0, + borderRadius: 10, + border: + foundOrg.id === userdata.active_org.id + ? `3px solid ${boxColor}` + : null, + cursor: "pointer", + marginRight: 10, + }; - image = - foundOrg.image === "" ? ( - {foundOrg.name} - ) : ( - {foundOrg.name} {}} - /> - ); + image = + foundOrg.image === "" ? ( + {foundOrg.name} + ) : ( + {foundOrg.name} { }} + /> + ); - orgName = foundOrg.name; - orgId = foundOrg.id; - } + orgName = foundOrg.name; + orgId = foundOrg.id; + } } return ( @@ -776,185 +1922,185 @@ const NotificationItem = memo((props) => { backgroundColor: theme.palette.cardHoverColor, }, }} - > -
- {data.amount === 1 && data.read === false ? - - : null} - {data.ignored === true ? - - : null} - {data.read === false ? - - : - - } - - {data.title} - -
- - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.title} - : - null - } - - {data.description} - -
- - - - {data.read === false ? ( - - ) : null} - - - - - +
+ {data.amount === 1 && data.read === false ? + + : null} + {data.ignored === true ? + + : null} + {data.read === false ? + + : + + } + + {data.title} + +
- 0 ? + {data.title} + : + null + } + + {data.description} + +
+ + - }} - > - First seen:{" "} - {new Date(data.created_at * 1000).toISOString().slice(0, 19)} - + {data.read === false ? ( + + ) : null} - - Last seen:{" "} - {new Date(data.updated_at * 1000).toISOString().slice(0, 19)} - + + + + - - Times seen: {data.amount} - -
+ + }} + > + First seen:{" "} + {new Date(data.created_at * 1000).toISOString().slice(0, 19)} + + + + Last seen:{" "} + {new Date(data.updated_at * 1000).toISOString().slice(0, 19)} + + + + Times seen: {data.amount} + +
+ + ); }) -const NotificationComponent = memo(({notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification}) => { +const NotificationComponent = memo(({ notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification }) => { - return( + return (
{notifications === null || notifications === undefined || notifications?.length === 0 ? ( - null - ) : -
- {notifications?.map((notification, index) => { - if (showRead === false && notification.read === true) { - return null - } + null + ) : +
+ {notifications?.map((notification, index) => { + if (showRead === false && notification.read === true) { + return null + } - return ( - - ) - })} -
- } + return ( + + ) + })} +
+ }
) }) diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index 2248c318..a77560dd 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -47,7 +47,7 @@ const chipStyle = { backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", } -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const SearchData = props => { const { serverside, globalUrl, userdata } = props let navigate = useNavigate(); diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 33b37483..3132034f 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -132,7 +132,7 @@ const CodeEditor = (props) => { fieldname, contentLoading, editorData, - handleSubflowParamChange, + handleTriggerParamChange, setAiQueryModalOpen, fullScreenMode, environment, @@ -212,7 +212,7 @@ const CodeEditor = (props) => { const triggerField = searchParams.get('trigger_field'); const triggerName = searchParams.get('trigger_name'); const conditionId = searchParams.get('condition_id'); - const conditionField = searchParams.get('field'); + const conditionField = searchParams.get('condition_field'); useEffect(() => { if (actionId === undefined || actionId === null) { @@ -251,7 +251,7 @@ const CodeEditor = (props) => { setSelectedCondition(condition); // Update available variables when condition changes updateAvailableVariables(actionlist); - }, [conditionId, fieldName]) + }, [conditionId, conditionField]) // Extract variable updating logic into a separate function const updateAvailableVariables = (actionlist) => { @@ -2588,7 +2588,7 @@ const CodeEditor = (props) => { // Handle condition fields if (conditionField !== null && handleConditionFieldChange !== undefined) { - handleConditionFieldChange(conditionField, fieldName, fixedcodedata); + handleConditionFieldChange(conditionField, fixedcodedata); } // Handle action fields else if (actionId !== undefined && actionId !== null && actionId.length > 0) { @@ -2596,7 +2596,7 @@ const CodeEditor = (props) => { } // Handle trigger fields else if (triggerId !== undefined && triggerId !== null && triggerId.length > 0) { - handleSubflowParamChange(triggerId, triggerField, fixedcodedata) + handleTriggerParamChange(triggerId, triggerField, fixedcodedata) } setExpansionModalOpen(false) diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index 2346a5a4..d70465f6 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -1359,7 +1359,7 @@ const UserManagmentTab = memo((props) => { }} > - {["Username", /*"API Key",*/ "Role", /*"Active",*/ "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => ( + {["Region", "Username", /*"API Key",*/ "Role", /*"Active",*/ "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => ( { ); } + const getRegionFlag = (region_url) => { + let regiontag = "UK"; + let regionCode = "gb"; + const regionsplit = region_url?.split("."); + if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) { + const namesplit = regionsplit[0]?.split("/"); + regiontag = namesplit[namesplit?.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + }else if (regiontag === "au") { + regiontag = "AUS"; + regionCode = "au" + } + } + + return regionCode; + }; + + const userRegion = data?.user_geo_info?.country?.iso_code?.length > 0 ? data?.user_geo_info?.country?.iso_code : data?.active_org?.region_url?.length > 0 ? getRegionFlag(data?.active_org?.region_url) : "eu"; + return ( + )} + style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'center' }} + /> diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 5de17156..218cc060 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -24,7 +24,7 @@ import { import WorkflowPaper from "../components/WorkflowPaper.jsx" import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const AppGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props diff --git a/frontend/src/components/Workflowsearch.jsx b/frontend/src/components/Workflowsearch.jsx index b8f4c022..1b342645 100644 --- a/frontend/src/components/Workflowsearch.jsx +++ b/frontend/src/components/Workflowsearch.jsx @@ -10,7 +10,7 @@ import algoliasearch from 'algoliasearch'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material'; -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const WorkflowSearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx index aaff1429..15a82580 100644 --- a/frontend/src/views/Admin2.jsx +++ b/frontend/src/views/Admin2.jsx @@ -334,7 +334,8 @@ const Admin2 = (props) => { } return ( -
+ //
+
); diff --git a/frontend/src/views/AdminSetup.jsx b/frontend/src/views/AdminSetup.jsx index 0365cf2f..65467300 100755 --- a/frontend/src/views/AdminSetup.jsx +++ b/frontend/src/views/AdminSetup.jsx @@ -68,13 +68,19 @@ const AdminAccount = (props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); + setLoginInfo(responseJson["reason"]) + + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + navigate("/loginsetup") + } + } else { if (responseJson.reason === "redirect") { setTimeout(() => { - window.location.pathname = "/login"; + window.location.pathname = "/login" }, 2500) } + } }) ) @@ -111,7 +117,7 @@ const AdminAccount = (props) => { if (responseJson["success"] === false) { setLoginInfo(responseJson["reason"]); } else { - setLoginInfo("Successful register :)"); + setLoginInfo("Successful register! Redirecting in a moment..."); setTimeout(() => { window.location.pathname = "/login"; diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index fba1d24a..7526c9d7 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -438,7 +438,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); //const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const AngularWorkflow = (defaultprops) => { const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; const {themeMode, supportEmail, brandColor} = useContext(Context) @@ -790,7 +790,7 @@ const AngularWorkflow = (defaultprops) => { }, { "name": "fields", - "value": "", + "value": '{\n "ticket_id": "123456",\n "comment": "This is a comment"\n}', "required": false, "multiline": true, }, @@ -3573,6 +3573,12 @@ const AngularWorkflow = (defaultprops) => { if (curapp?.actions === undefined || curapp?.actions === null || curapp?.actions?.length === 0 || curapp?.actions?.length === 1) { loadAppConfig(curapp?.id, false, true) } + + if (key > 10) { + console.log("Breaking on 10 sideloads of total", responseJson.length) + break + + } } // Find app with ID "794e51c3c1a8b24b89ccc573a3defc47" (gmail) to force-break it, @@ -7000,7 +7006,11 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (responseJson.success === false) { - toast("Failed to auto-activate the app. Go to /apps and activate it.") + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + toast.error("Failed to auto-activate the app: " + responseJson.reason) + } else { + toast.error("Failed to auto-activate the app. Go to /apps and activate it.") + } } else { if (refresh === true) { setHighlightedApp(appid) @@ -10312,7 +10322,7 @@ const AngularWorkflow = (defaultprops) => { } } - toast("Creating schedule") + toast.info("Creating schedule") var data = { name: trigger.name, frequency: workflow.triggers[triggerindex].parameters[0].value, @@ -10361,9 +10371,9 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (!responseJson.success) { - toast("Failed to set schedule: " + responseJson.reason); + toast.error("Failed to set schedule: " + responseJson.reason); } else { - toast("Successfully created schedule"); + toast.success("Successfully created schedule"); workflow.triggers[triggerindex].status = "running"; trigger.status = "running"; setSelectedTrigger(trigger); @@ -11852,7 +11862,7 @@ const AngularWorkflow = (defaultprops) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -11876,8 +11886,9 @@ const AngularWorkflow = (defaultprops) => { var type = "app" const baseImage = + const width = 230 return ( -
+
{hits.length === 0 ? @@ -11971,7 +11982,7 @@ const AngularWorkflow = (defaultprops) => { }} defaultPosition={{ x: 0, y: 0 }} > -
{ +
{ clickedApp(hit) }}> @@ -12125,7 +12136,12 @@ const AngularWorkflow = (defaultprops) => { } if ((app.id === "integration" || app.id === "shuffle_agent") && userdata.support !== true) { - return null + console.log("APPID: ", app.id, isCloud) + if (isCloud === false && app.id === "integration") { + } else { + console.log("RETURNING", app.id) + return null + } } if (viewedApps.includes(app.id)) { @@ -12190,7 +12206,7 @@ const AngularWorkflow = (defaultprops) => {
) : apps.length > 0 ? (
{ console.log("Should load in extra apps?") }} @@ -12198,6 +12214,7 @@ const AngularWorkflow = (defaultprops) => { Couldn't find the apps you were looking for? Searching unactivated apps. Click one of these apps to Activate it for your organisation. + { console.log("CLICKED") }}> @@ -13021,7 +13038,7 @@ const AngularWorkflow = (defaultprops) => { event.preventDefault() setExpansionModalOpen(true) setActiveDialog("codeeditor") - navigate(`?condition_id=${data.id}&field=${data.name}`) + navigate(`?condition_id=${data.id}&condition_field=${data.name}`) setEditorData({ "name": data.name, "value": data.value || "", @@ -13106,9 +13123,9 @@ const AngularWorkflow = (defaultprops) => { // Update the field value based on type if (type === "source") { - handleConditionFieldChange("source", "value", toComplete); + handleConditionFieldChange("source", toComplete); } else if (type === "destination") { - handleConditionFieldChange("destination", "value", toComplete); + handleConditionFieldChange("destination", toComplete); } handleMenuClose(); @@ -13985,7 +14002,7 @@ const AngularWorkflow = (defaultprops) => { /> - const handleConditionFieldChange = (fieldType, fieldName, value) => { + const handleConditionFieldChange = (fieldType, value) => { if (fieldType === "source") { setSourceValue({ ...sourceValue, @@ -15186,7 +15203,7 @@ const AngularWorkflow = (defaultprops) => { } ] - const handleSubflowParamChange = (triggerId, triggerField, newData) => { + const handleTriggerParamChange = (triggerId, triggerField, newData) => { var updateFail = "" if (workflow !== undefined && workflow !== null) { @@ -15303,7 +15320,8 @@ const AngularWorkflow = (defaultprops) => { Name { Delay { fullWidth rows="4" multiline - defaultValue={selectedTrigger.parameters[0]?.value} + value={selectedTriggerValue || ""} color="primary" placeholder="" + onChange={(e) => { + setLastSaved(false) + setSelectedTriggerValue(e.target.value) + }} onBlur={(e) => { setLastSaved(false) setTriggerTextInformationWrapper(e.target.value); @@ -18681,7 +18704,9 @@ const AngularWorkflow = (defaultprops) => { {originalWorkflow?.suborg_distribution === undefined || originalWorkflow?.suborg_distribution === null || originalWorkflow?.suborg_distribution?.length === 0 || originalWorkflow?.suborg_distribution.includes("none") ? - originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0 ? +
+ {originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0 ? + - : userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 ? + + : null} + + {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 ? - : null - + : null} +
: { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -25459,7 +25487,7 @@ const AngularWorkflow = (defaultprops) => { // selectedTrigger={selectedTrigger} aiSubmit={aiSubmit} toolsAppId={toolsApp.id} - handleSubflowParamChange={handleSubflowParamChange} + handleTriggerParamChange={handleTriggerParamChange} codedata={editorData.value} setcodedata={setcodedata} selectedEdge={selectedEdge} diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index 45f9d762..d104d43d 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -50,7 +50,7 @@ import { green } from "../views/AngularWorkflow.jsx" const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ) // Lazy loading of ApiExplorer component to reduce initial load time diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index c2b138a9..94bd3ed3 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -93,7 +93,7 @@ import aa from "search-insights"; // 2 = OpenAPI (Invalid) const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ) const AppExplorer = (props) => { @@ -3996,7 +3996,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; if (queryID !== undefined && queryID !== null) { aa("init", { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }); const timestamp = new Date().getTime(); @@ -4085,7 +4085,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; if (queryID !== undefined && queryID !== null) { aa("init", { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }); const timestamp = new Date().getTime(); diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 8b4eadd4..684d8e9c 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -280,7 +280,7 @@ export const GetParsedPaths = (inputdata, basekey) => { return parsedValues; }; -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Apps = (props) => { const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props; @@ -1305,7 +1305,7 @@ const Apps = (props) => { if (queryID !== undefined && queryID !== null) { aa("init", { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }); const timestamp = new Date().getTime(); @@ -2036,7 +2036,7 @@ const Apps = (props) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx index d4d1c822..8c8e4284 100644 --- a/frontend/src/views/Apps2.jsx +++ b/frontend/src/views/Apps2.jsx @@ -46,7 +46,7 @@ import AppCreationModal from "../components/AppCreationModal.jsx"; const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ); // AppCard Component @@ -1122,6 +1122,7 @@ const Apps2 = (props) => { const [defaultSearch, setDefaultSearch] = useState(""); const [apps, setApps] = useState([]); + const [backupApps, setBackupApps] = useState([]); const [filteredApps, setFilteredApps] = useState([]); const [appSearchLoading, setAppSearchLoading] = useState(false); const [creatorProfile, setCreatorProfile] = useState({}); @@ -1198,57 +1199,9 @@ const Apps2 = (props) => { getFramework(); }, []); - // Fetch apps based on the current tab : 0 -> org_apps, 1 -> my_apps, 2 -> all_apps - const fetchApps = async () => { - const baseUrl = globalUrl; - let url; - setIsLoading(true); - const userId = userdata?.id; - if (currTab === 1 && userId) { - url = `${baseUrl}/api/v1/users/${userId}/apps`; - } else if (currTab === 0) { - url = `${baseUrl}/api/v1/apps`; - } - try { - const response = await fetch(url, { - method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await response.json(); - if (currTab === 1) { - setAppsToShow(data); - setUserApps(data); - } else if (currTab === 0) { - setAppsToShow(data); - setOrgApps(data); - // For testing the empty state - // setAppsToShow([]); - // setOrgApps([]); - } - setIsLoading(false); - } catch (err) { - console.error("Error fetching apps:", err); - setIsLoading(false); - } - }; - useEffect(() => { - - // Only fetch if we have required data - if (globalUrl && (currTab === 0 || (currTab === 1 && userdata?.id))) { - fetchApps(); - } - }, [currTab, globalUrl, userdata?.id]); // Remove location.search dependency - - // useEffect(() => { - // // setSearchQuery(""); - // setSelectedCategory([]); - // setSelectedLabel([]); - // }, [currTab]) - + getApps() + }, []) // Find top categories and tags based on the current tab useEffect(() => { @@ -1293,11 +1246,13 @@ const Apps2 = (props) => { }); }; + /* useEffect(() => { if (serverside) { return null; } }, [serverside]); + */ const getApps = () => { // Get apps from localstorage @@ -1308,7 +1263,7 @@ const Apps2 = (props) => { if (storageApps === null || storageApps === undefined || storageApps.length === 0) { storageApps = [] } else { - setAppsToShow(storageApps) + //setAppsToShow(storageApps) setOrgApps(storageApps) setApps(storageApps) // setFilteredApps(storageApps) @@ -1344,18 +1299,25 @@ const Apps2 = (props) => { var privateapps = []; var valid = []; var invalid = []; + + var backups = [] for (var key in responseJson) { const app = responseJson[key]; - if (app.categories !== undefined && app.categories !== null && app?.categories.includes("Eradication")) { + if (app?.reference_info?.onprem_backup === true) { + backups.push(app) + continue + } + + if (app?.categories !== undefined && app?.categories !== null && app?.categories?.includes("Eradication")) { app.categories = ["EDR"] } - if (app.is_valid && !(!app.activated && app.generated)) { + if (app?.is_valid && !(!app?.activated && app?.generated)) { privateapps.push(app); } else if ( - app.private_id !== undefined && - app.private_id.length > 0 + app?.private_id !== undefined && + app?.private_id.length > 0 ) { valid.push(app); } else { @@ -1363,6 +1325,11 @@ const Apps2 = (props) => { } } + console.log("BACKUPAPPS: ", backups) + if (backups.length > 0) { + setBackupApps(backups) + } + privateapps.push(...valid); privateapps.push(...invalid); console.log("privateapps: setting apps ", privateapps) @@ -1372,39 +1339,22 @@ const Apps2 = (props) => { // setFilteredApps(privateapps); if (privateapps.length > 0) { - if (selectedApp.id === undefined || selectedApp.id === null) { - if (privateapps[0].owner !== undefined && privateapps[0].owner !== null) { - getUserProfile(privateapps[0].owner); + if (selectedApp?.id === undefined || selectedApp?.id === null) { + if (privateapps[0]?.owner !== undefined && privateapps[0]?.owner !== null) { + getUserProfile(privateapps[0]?.owner); } - - // setContact(privateapps[0].contact_info) - - // setSelectedApp(privateapps[0]); - // setSharingConfiguration(privateapps[0].sharing === true ? "public" : "you") } - - // if ( - // privateapps[0].actions !== null && - // privateapps[0].actions.length > 0 - // ) { - // setSelectedAction(privateapps[0].actions[0]); - // } else { - // setSelectedAction({}); - // } } - if (privateapps.length > 0 && storageApps.length === 0) { + if (privateapps?.length > 0 && storageApps?.length === 0) { try { localStorage.setItem("apps", JSON.stringify(privateapps)) } catch (e) { console.log("Failed to set apps in localstorage: ", e) } } - - //setTimeout(() => { - // setFirstLoad(false) - //}, 5000) }) .catch((error) => { + console.log("Failed to get apps: ", error.toString()); toast(error.toString()); setIsLoading(false); }); @@ -1780,7 +1730,6 @@ const Apps2 = (props) => { // setOpenModal(true); }; - useEffect(() => { const apps = currTab === 1 ? userApps : orgApps; const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel); @@ -1798,33 +1747,38 @@ const Apps2 = (props) => { } else if (newTab === 1) { const filteredUserApps = filterApps(userApps, searchQuery, selectedCategory, selectedLabel); setAppsToShow(filteredUserApps); - } + } else if (newTab === 3) { + const filteredUserApps = filterApps(backupApps, searchQuery, selectedCategory, selectedLabel); + setAppsToShow(filteredUserApps); + return + } // Update URL query params based on tab index const tabMapping = { 0: 'org_apps', 1: 'my_apps', - 2: 'all_apps' + 2: 'all_apps', + 3: 'backup_apps', }; - const queryParams = new URLSearchParams(location.search); - queryParams.set('tab', tabMapping[newTab]); + const queryParams = new URLSearchParams(location.search); + queryParams.set('tab', tabMapping[newTab]); - // Maintain search query in URL regardless of tab - if (searchQuery) { - queryParams.set('q', searchQuery); - } else { - queryParams.delete('q'); - } + // Maintain search query in URL regardless of tab + if (searchQuery) { + queryParams.set('q', searchQuery); + } else { + queryParams.delete('q'); + } - navigate(`${location.pathname}?${queryParams.toString()}`); + navigate(`${location.pathname}?${queryParams.toString()}`); }; // Update useEffect for filtering without URL manipulation useEffect(() => { if (currTab === 2) return; // Skip for "Discover Apps" tab as it uses Algolia - const apps = currTab === 1 ? userApps : orgApps; + const apps = currTab === 1 ? userApps : currTab === 3 ? backupApps : orgApps; const filteredApps = filterApps(apps, searchQuery, selectedCategory, selectedLabel); setAppsToShow(filteredApps); }, [searchQuery, selectedCategory, selectedLabel, currTab, userApps, orgApps]); @@ -1914,7 +1868,7 @@ const Apps2 = (props) => {
- {currTab === 0 ? "Org" : currTab === 1 ? "Your" : "Discover"} Apps + {currTab === 0 ? "Org" : currTab === 1 ? "Your" : currTab === 3 ? "Backup" : "Discover"} Apps {isCloud ? null : ( @@ -1940,7 +1894,7 @@ const Apps2 = (props) => { style={{ height: 45, minWidth: 45, - backgroundColor: "#2F2F2F", + backgroundColor: theme.palette.platformColor, borderRadius: 4, padding: "8px 16px", }} @@ -1950,9 +1904,9 @@ const Apps2 = (props) => { }} > {isLoading ? ( - + ) : ( - + )} @@ -1980,7 +1934,7 @@ const Apps2 = (props) => { style={{ height: 45, minWidth: 45, - backgroundColor: "#2F2F2F", + backgroundColor: theme.palette.platformColor, borderRadius: 4, padding: "8px 16px", }} @@ -1992,9 +1946,9 @@ const Apps2 = (props) => { }} > {isLoading ? ( - + ) : ( - + )} @@ -2027,6 +1981,7 @@ const Apps2 = (props) => { ...(currTab === 1 ? tabActive : {}) }} /> + { ...(currTab === 2 ? tabActive : {}) }} /> + + {backupApps.length > 0 && + + }
@@ -2043,7 +2009,7 @@ const Apps2 = (props) => { minWidth: "25%", maxWidth: "25%" }}> - {(currTab === 0 || currTab === 1) ? ( + {(currTab === 0 || currTab === 1 || currTab === 3) ? ( {
{ - currTab === 0 && ( + currTab === 0 || currTab === 3 && (
{isLoading ? ( @@ -2285,7 +2251,7 @@ const Apps2 = (props) => { handleAppClick={handleAppClick} leftSideBarOpenByClick={leftSideBarOpenByClick} userdata={userdata} - fetchApps={fetchApps} + fetchApps={getApps} setUserApps={setUserApps} appsToShow={appsToShow} @@ -2338,7 +2304,7 @@ const Apps2 = (props) => { {appsToShow.map((data, index) => ( { }, }) - if (serverside !== true) { - const tmpMessage = new URLSearchParams(window.location.search).get("message") - if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { - setMessage(tmpMessage) + useEffect(() => { + if (serverside !== true) { + const tmpMessage = new URLSearchParams(window.location.search).get("message") + if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { + setMessage(tmpMessage) + toast(tmpMessage) + } } - } + }, []) if (document !== undefined) { if (register) { @@ -361,7 +364,7 @@ const LoginPage = props => { console.log("Should login instead of register!") setRegister(!register) } else { - console.log("Path: " + path, "Register: " + register) + //console.log("Path: " + path, "Register: " + register) } } @@ -444,13 +447,17 @@ const LoginPage = props => { } if (isLoggedIn === true && serverside !== true) { - const tmpView = new URLSearchParams(window.location.search).get("view") - if (tmpView !== undefined && tmpView !== null && tmpView === "pricing") { - window.location.pathname = "/pricing" - return - } else if (tmpView !== undefined && tmpView !== null) { - window.location.pathname = tmpView - return + const tmpView = new URLSearchParams(window.location.search).get("view"); + if (tmpView !== undefined && tmpView !== null) { + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; + + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + window.location.replace(pathOnly); + } + return; } window.location.pathname = "/workflows" @@ -468,6 +475,11 @@ const LoginPage = props => { response.json().then((responseJson) => { if (responseJson["success"] === false) { setLoginInfo(responseJson["reason"]); + + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + navigate("/loginsetup") + } + } else { if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { setSSOUrl(responseJson.sso_url); @@ -570,20 +582,17 @@ const LoginPage = props => { setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }) } - const tmpView = new URLSearchParams(window.location.search).get("view") + const tmpView = new URLSearchParams(window.location.search).get("view"); if (tmpView !== undefined && tmpView !== null) { - //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` - // Check if slash in the url + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; - var newUrl = `/${tmpView}` - if (tmpView.startsWith("/")) { - newUrl = `${tmpView}` + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + window.location.replace(pathOnly); } - - console.log("Found url: ", newUrl) - - window.location.pathname = newUrl - return + return; } console.log("LOGIN DATA: ", responseJson) @@ -642,9 +651,14 @@ const LoginPage = props => { const tmpView = new URLSearchParams(window.location.search).get("view") if (tmpView !== undefined && tmpView !== null) { - //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` - const newUrl = `/${tmpView}` - window.location.pathname = newUrl + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; + + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + window.location.replace(pathOnly); + } return } diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx index 541cd903..aaee73d6 100755 --- a/frontend/src/views/LoginPageOld.jsx +++ b/frontend/src/views/LoginPageOld.jsx @@ -85,7 +85,13 @@ const LoginDialog = (props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); + setLoginInfo(responseJson["reason"]) + + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + setLoginViewLoading(true) + start() + } + } else { if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { diff --git a/frontend/src/views/SettingsPage.jsx b/frontend/src/views/SettingsPage.jsx index f10dbfb7..ec5ba658 100755 --- a/frontend/src/views/SettingsPage.jsx +++ b/frontend/src/views/SettingsPage.jsx @@ -458,7 +458,12 @@ const Settings = (props) => { if (responseJson["success"] === false) { setPasswordFormMessage(responseJson["reason"]); } else { - toast("Changed password!"); + var reason = "" + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + reason += responseJson.reason + } + + toast.success("Changed password! " + reason); setPasswordFormMessage(""); } }) diff --git a/frontend/src/views/Usecases2.jsx b/frontend/src/views/Usecases2.jsx index 950da807..036c26a4 100644 --- a/frontend/src/views/Usecases2.jsx +++ b/frontend/src/views/Usecases2.jsx @@ -126,6 +126,13 @@ const UsecaseListComponent = (props) => { const { themeMode, brandName } = useContext(Context) const theme = getTheme(themeMode) + const usecaseLightThemeColor = { + "collect": "#FB47A0", + "enrich": "#F38B14", + "detect": "#0AAD65", + "respond": "#289BDB", + "verify": "#624CE9", + } const [expandedIndex, setExpandedIndex] = useState(-1); const [expandedItem, setExpandedItem] = useState(-1); const [inputUsecase, setInputUsecase] = useState({}); @@ -743,7 +750,7 @@ const UsecaseListComponent = (props) => { {keys.map((usecase, index) => { return (
- + {index+1}. {usecase.name.slice(3, 100)} diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index 1655c901..9753bfb0 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -111,7 +111,7 @@ import { removeQuery } from "../components/ScrollToTop.jsx"; import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240"); +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e"); const svgSize = 24; const imagesize = 22; @@ -678,6 +678,7 @@ const Workflows2 = (props) => { var upload = ""; const [workflows, setWorkflows] = React.useState([]); + const [backupWorkflows, setBackupWorkflows] = React.useState([]); const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [selectedUsecases, setSelectedUsecases] = React.useState([]); const [filteredWorkflows, setFilteredWorkflows] = React.useState([]); @@ -756,7 +757,8 @@ const Workflows2 = (props) => { const tabMapping = { 0: 'org_workflows', 1: 'my_workflows', - 2: 'all_workflows' + 2: 'all_workflows', + 3: 'backup_apps', }; const queryParams = new URLSearchParams(location.search); queryParams.set('tab', tabMapping[newValue]); @@ -1107,23 +1109,31 @@ const Workflows2 = (props) => { setSelectedWorkflowId(""); }} PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: 500, - padding: 50, - }, - }} + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: '440px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} > -
+
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}?
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working
); diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 3697735e..4b3076b6 100755 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -610,13 +610,31 @@ func deployServiceWorkers(image string) { if defaultNetworkAttach == true || strings.ToLower(os.Getenv("SHUFFLE_DEFAULT_NETWORK_ATTACH")) == "true" { targetName := "shuffle_shuffle" - log.Printf("[DEBUG] Adding network attach for network %s to worker in swarm", targetName) - serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{ - Target: targetName, - }) + isAttachable := false + networks, err := dockercli.NetworkList(ctx, network.ListOptions{}) + if err == nil { + for _, net := range networks { + if net.Name == targetName { + if net.Scope == "swarm" { + log.Printf("[DEBUG] Found swarm-scoped network: %s", targetName) + isAttachable = true + } else { + log.Printf("[WARNING] Network %s exist but is not swarm scoped (scope=%s)", targetName, net.Scope) + } + break + } + } + } - // FIXM: Remove this if deployment fails? - serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=%s", targetName)) + if isAttachable { + log.Printf("[DEBUG] Adding network attach for network %s to worker in swarm", targetName) + serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{ + Target: targetName, + }) + + // FIXM: Remove this if deployment fails? + serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=%s", targetName)) + } } if dockerApiVersion != "" { @@ -709,6 +727,34 @@ func deployServiceWorkers(image string) { } else { if !strings.Contains(fmt.Sprintf("%s", err), "Already Exists") && !strings.Contains(fmt.Sprintf("%s", err), "is already in use by service") { log.Printf("[ERROR] Failed making service: %s", err) + if strings.Contains(fmt.Sprintf("%s", err), "networks scoped to the swarm can be used") { + log.Printf("[WARNING] Swarm network attachment failed, retrying without shuffle_shuffle") + + var updatedNetworks []swarm.NetworkAttachmentConfig + for _, net := range serviceSpec.Networks { + if net.Target != "shuffle_shuffle" { + updatedNetworks = append(updatedNetworks, net) + } + } + serviceSpec.Networks = updatedNetworks + + var updatedEnv []string + for _, env := range serviceSpec.TaskTemplate.ContainerSpec.Env { + if !strings.HasPrefix(env, "SHUFFLE_SWARM_OTHER_NETWORK=") { + updatedEnv = append(updatedEnv, env) + } + } + serviceSpec.TaskTemplate.ContainerSpec.Env = updatedEnv + serviceOptions := types.ServiceCreateOptions{} + _, err = dockercli.ServiceCreate( + ctx, + serviceSpec, + serviceOptions, + ) + if err != nil { + log.Printf("[ERROR] Failed to deploy service even without shuffle_shuffle network: %s", err) + } + } } else { log.Printf("[WARNING] Failed deploying workers: %s", err) if len(serviceSpec.Networks) > 1 {