From d367c76838878a2860ee87652e857294b6a995b1 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 5 Nov 2020 04:15:01 +0100 Subject: [PATCH 01/18] Cloud sync trigger progress --- backend/go-app/main.go | 334 ++++++++++++++++++++++++++++------- backend/go-app/walkoff.go | 27 +-- frontend/src/App.jsx | 1 + frontend/src/views/Admin.jsx | 6 +- 4 files changed, 295 insertions(+), 73 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index e88e8a9e..c73bcd72 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1306,11 +1306,18 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { return } + http.SetCookie(resp, &http.Cookie{ + Name: "session_token", + Value: "", + Path: "/", + Expires: time.Unix(0, 0), + }) + userInfo, err := handleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in handleLogout: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true, "reason": "Not logged in"}`)) return } @@ -1386,7 +1393,6 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { } //memcache.Delete(request.Context(), sessionToken) - //http.SetCookie(resp, c) resp.WriteHeader(200) resp.Write([]byte(`{"success": false, "reason": "Successfully logged out"}`)) @@ -2231,7 +2237,7 @@ func handleGetSchedules(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - schedules, err := getAllSchedules(ctx) + schedules, err := getAllSchedules(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed getting schedules: %s", err) resp.WriteHeader(401) @@ -6466,6 +6472,133 @@ func handleAppHotload(location string, forceUpdate bool) error { return nil } +// Primary = usually an outer ID, e.g. workflow ID +// Secondary = something to specify what inside workflow to execute +// Third = Some data to add to it +type CloudSyncJob struct { + Type string `json:"type" datastore:"type"` + Action string `json:"action" datastore:"action"` + OrgId string `json:"org_id" datastore:"org_id"` + PrimaryItemId string `json:"primary_item_id" datastore:"primary_item_id"` + SecondaryItem string `json:"secondary_item" datastore:"secondary_item"` + ThirdItem string `json:"third_item" datastore:"third_item"` + FourthItem string `json:"fourth_item" datastore:"fourth_item"` + Created string `json:"created" datastore:"created"` +} + +func handleCloudJob(job CloudSyncJob) error { + log.Printf("Handle job with type %s and action %s", job.Type, job.Action) + if job.Type == "webhook" { + if job.Action == "execute" { + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + } + + } else if job.Type == "user_input" { + log.Printf("Should handle user_input for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + } else if job.Type == "schedule" { + log.Printf("Should handle schedule for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + } else if job.Type == "email" { + log.Printf("Should handle email for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + } else { + log.Printf("No handler for type %s", job.Type) + } + + return nil +} + +// Handles jobs from remote (cloud) +func remoteOrgJobController(org Org, body []byte) error { + type retStruct struct { + Success bool `json:"success"` + Reason string `json:"reason"` + Jobs []CloudSyncJob `json:"jobs"` + } + + log.Printf("Remote JOB ret: %s", string(body)) + responseData := retStruct{} + err := json.Unmarshal(body, &responseData) + if err != nil { + return err + } + + ctx := context.Background() + if !responseData.Success { + log.Printf("Should stop org job controller") + + if strings.Contains(responseData.Reason, "Bad apikey") { + log.Printf("Bad apikey. Stopping sync for org!") + + if value, exists := scheduledOrgs[org.Id]; exists { + // Looks like this does the trick? Hurr + log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) + + value.Lock() + org, err := getOrg(ctx, org.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return err + } + + org.SyncConfig.Interval = 0 + org.SyncConfig.Apikey = "" + org.CloudSync = false + err = setOrg(ctx, *org, org.Id) + if err != nil { + log.Printf("Failed setting organization when stopping sync: %s", err) + } else { + log.Printf("Successfully updated the org to not sync") + } + + return errors.New("Stopped schedule for org because of bad apikey.") + } else { + return errors.New(fmt.Sprintf("Failed finding the schedule for org %s", org.Id)) + } + } + + return errors.New("[ERROR] Remote job handler issues.") + } + + log.Printf("Got job with reason %s and %d jobs", responseData.Reason, len(responseData.Jobs)) + for _, job := range responseData.Jobs { + err = handleCloudJob(job) + if err != nil { + log.Printf("[ERROR] Failed job from cloud: %s", err) + } + } + + return nil +} + +func remoteOrgJobHandler(org Org, interval int) error { + client := &http.Client{} + syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) + req, err := http.NewRequest( + "GET", + syncUrl, + nil, + ) + + req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey)) + newresp, err := client.Do(req) + if err != nil { + log.Printf("Failed request in org sync: %s", err) + return err + } + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("Failed body read in job sync: %s", err) + return err + } + + err = remoteOrgJobController(org, respBody) + if err != nil { + log.Printf("Failed job controller run: %s", err) + return err + } + return nil +} + // Handles configuration items during Shuffle startup func runInit(ctx context.Context) { // Setting stats for backend starts (failure count as well) @@ -6739,7 +6872,8 @@ func runInit(ctx context.Context) { } else { updated := 0 for _, workflow := range workflows { - if workflow.ExecutingOrg.Id == "" { + if workflow.ExecutingOrg.Id == "" || len(workflow.OrgId) == 0 { + workflow.OrgId = activeOrgs[0].Id workflow.ExecutingOrg = activeOrgs[0] err = setWorkflow(ctx, workflow, workflow.ID) @@ -6757,42 +6891,115 @@ func runInit(ctx context.Context) { } } - fileq := datastore.NewQuery("Files").Limit(1) - count, err := dbclient.Count(ctx, fileq) + /* + fileq := datastore.NewQuery("Files").Limit(1) + count, err := dbclient.Count(ctx, fileq) - if err == nil && count == 0 { - basepath := "." - filename := "testfile.txt" - fileId := uuid.NewV4().String() - log.Printf("Creating new file reference %s because none exist!", fileId) - workflowId := "2e9d6474-402c-4dcc-bb53-45f638ca18d3" - downloadPath := fmt.Sprintf("%s/%s/%s/%s", basepath, activeOrgs[0].Id, workflowId, fileId) + if err == nil && count == 0 { + basepath := "." + filename := "testfile.txt" + fileId := uuid.NewV4().String() + log.Printf("Creating new file reference %s because none exist!", fileId) + workflowId := "2e9d6474-402c-4dcc-bb53-45f638ca18d3" + downloadPath := fmt.Sprintf("%s/%s/%s/%s", basepath, activeOrgs[0].Id, workflowId, fileId) - timeNow := time.Now().Unix() - newFile := File{ - Id: fileId, - CreatedAt: timeNow, - UpdatedAt: timeNow, - Description: "Created by system for testing", - Status: "active", - Filename: filename, - OrgId: activeOrgs[0].Id, - WorkflowId: workflowId, - DownloadPath: downloadPath, + timeNow := time.Now().Unix() + newFile := File{ + Id: fileId, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Description: "Created by system for testing", + Status: "active", + Filename: filename, + OrgId: activeOrgs[0].Id, + WorkflowId: workflowId, + DownloadPath: downloadPath, + } + + err = setFile(ctx, newFile) + if err != nil { + log.Printf("Failed setting file: %s", err) + } else { + log.Printf("Created file %s in init", newFile.DownloadPath) + } } + */ - err = setFile(ctx, newFile) + var allworkflowapps []AppAuthenticationStorage + q = datastore.NewQuery("workflowappauth") + _, err = dbclient.GetAll(ctx, q, &allworkflowapps) + if err == nil { + log.Printf("Setting up all app auths with org %s", activeOrgs[0].Id) + for _, item := range allworkflowapps { + if item.OrgId != "" { + continue + } + + //log.Printf("Should update auth for %#v!", item) + item.OrgId = activeOrgs[0].Id + err = setWorkflowAppAuthDatastore(ctx, item, item.Id) + if err != nil { + log.Printf("Failed adding AUTH to org %s", activeOrgs[0].Id) + } + } + } + + var schedules []ScheduleOld + q = datastore.NewQuery("schedules") + _, err := dbclient.GetAll(ctx, q, &schedules) + if err == nil { + log.Printf("Setting up all schedules with org %s", activeOrgs[0].Id) + for _, item := range schedules { + if item.Org != "" { + continue + } + + item.Org = activeOrgs[0].Id + err = setSchedule(ctx, item) + if err != nil { + log.Printf("Failed adding schedule to org %s", activeOrgs[0].Id) + } + } + } + } + + log.Printf("Starting cloud schedules for orgs!") + type requestStruct struct { + ApiKey string `json:"api_key"` + } + for _, org := range activeOrgs { + if !org.CloudSync { + log.Printf("Skipping org %s because sync isn't set (1).", org.Id) + continue + } + + //interval := int(org.SyncConfig.Interval) + interval := 5 + if interval == 0 { + log.Printf("Skipping org %s because sync isn't set (0).", org.Id) + continue + } + + log.Printf("Should start schedule for org %s", org.Name) + job := func() { + err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("Failed setting file: %s", err) - } else { - log.Printf("Created file %s in init", newFile.DownloadPath) + log.Printf("Failed request with remote org setup: err") } } + + jobret, err := newscheduler.Every(int(interval)).Seconds().NotImmediately().Run(job) + if err != nil { + log.Printf("[CRITICAL] Failed to schedule org: %s", err) + } else { + log.Printf("Started sync on interval %d for org %s", interval, org.Name) + scheduledOrgs[org.Id] = jobret + } } // Gets schedules and starts them log.Printf("Relaunching schedules") - schedules, err := getAllSchedules(ctx) + schedules, err := getAllSchedules(ctx, "ALL") if err != nil { log.Printf("Failed getting schedules during service init: %s", err) } else { @@ -6927,6 +7134,7 @@ func runInit(ctx context.Context) { } } + log.Printf("Finished INIT") } @@ -7053,6 +7261,13 @@ func handleStopCloudSync(syncUrl string, org Org) error { } } + if value, exists := scheduledOrgs[org.Id]; exists { + // Looks like this does the trick? Hurr + log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) + + value.Lock() + } + return nil } @@ -7068,7 +7283,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { user, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("Api authentication failed in verify swagger: %s", err) + log.Printf("Api authentication failed in cloud setup: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -7254,6 +7469,23 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { Interval: responseData.IntervalSeconds, } + interval := int(responseData.IntervalSeconds) + log.Printf("Starting cloud sync on interval %d", interval) + job := func() { + err := remoteOrgJobHandler(*org, interval) + if err != nil { + log.Printf("Failed request with remote org setup: err") + } + } + + jobret, err := newscheduler.Every(int(interval)).Seconds().NotImmediately().Run(job) + if err != nil { + log.Printf("[CRITICAL] Failed to schedule org: %s", err) + } else { + log.Printf("Started sync on interval %d for org %s", interval, org.Name) + scheduledOrgs[org.Id] = jobret + } + // FIXME: Add this for every feature if org.SyncFeatures.Workflows.Active { log.Printf("Should activate cloud workflows for org %s!", org.Id) @@ -7325,34 +7557,14 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { } type File struct { - Id string `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - CreatedAt int64 `json:"created_at" datastore:"created_at"` - UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` - CreatedBy struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Login string `json:"login" datastore:"login"` - Name string `json:"name" datastore:"name"` - } `json:"created_by" datastore:"created_by"` - Description string `json:"description" datastore:"description"` - Etag int `json:"etag" datastore:"etag"` - ExpiresAt string `json:"expires_at" datastore:"expires_at"` - Folder struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Etag int `json:"etag" datastore:"etag"` - Name string `json:"name" datastore:"name"` - SequenceID int `json:"sequence_id" datastore:"sequence_id"` - } `json:"folder" datastore:"folder"` - Status string `json:"status" datastore:"status"` - Filename string `json:"filename" datastore:"filename"` - UpdatedBy struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Login string `json:"login" datastore:"login"` - Name string `json:"name" datastore:"name"` - } `json:"updated_by" datastore:"updated_by"` + Id string `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + CreatedAt int64 `json:"created_at" datastore:"created_at"` + UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` + Description string `json:"description" datastore:"description"` + ExpiresAt string `json:"expires_at" datastore:"expires_at"` + Status string `json:"status" datastore:"status"` + Filename string `json:"filename" datastore:"filename"` URL string `json:"url" datastore:"org"` OrgId string `json:"org_id" datastore:"org_id"` WorkflowId string `json:"workflow_id" datastore:"workflow_id"` @@ -7478,6 +7690,7 @@ func initHandlers() { r.HandleFunc("/api/v1/_ah/health", healthCheckHandler) // Make user related locations + // Fix user changes with org r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/login", handleLogin).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/logout", handleLogout).Methods("POST", "OPTIONS") @@ -7534,6 +7747,7 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/authentication", getAppAuthentication).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/apps/authentication", addAppAuthentication).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication/{appauthId}", deleteAppAuthentication).Methods("DELETE", "OPTIONS") // Legacy app things @@ -7547,9 +7761,9 @@ func initHandlers() { r.HandleFunc("/api/v1/workflows", getWorkflows).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows", setNewWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/schedules", handleGetSchedules).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/download_remote", loadSpecificWorkflows).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/execute", executeWorkflow).Methods("GET", "POST", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/schedule/{schedule}", stopSchedule).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/outlook", createOutlookSub).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index c6c08c2f..35f9e329 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -44,6 +44,7 @@ var baseEnvironment = "onprem" var cloudname = "cloud" var defaultLocation = "europe-west2" var scheduledJobs = map[string]*newscheduler.Job{} +var scheduledOrgs = map[string]*newscheduler.Job{} // To test out firestore before potential merge //var upgrader = websocket.Upgrader{ @@ -329,6 +330,7 @@ type Workflow struct { Sharing string `json:"sharing" datastore:"sharing"` Org []Org `json:"org,omitempty" datastore:"org"` ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"` + OrgId string `json:"org_id,omitempty" datastore:"org_id"` WorkflowVariables []struct { Description string `json:"description" datastore:"description,noindex"` ID string `json:"id" datastore:"id"` @@ -1283,7 +1285,7 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { // With user, do a search for workflows with user or user's org attached q := datastore.NewQuery("workflow").Filter("owner =", user.Id) if user.Role == "admin" { - q = datastore.NewQuery("workflow") + q = datastore.NewQuery("workflow").Filter("org_id =", user.ActiveOrg.Id) } var workflows []Workflow @@ -1363,6 +1365,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Owner = user.Id workflow.Sharing = "private" workflow.ExecutingOrg = user.ActiveOrg + workflow.OrgId = user.ActiveOrg.Id ctx := context.Background() log.Printf("Saved new workflow %s with name %s", workflow.ID, workflow.Name) @@ -3501,9 +3504,9 @@ func getWorkflow(ctx context.Context, id string) (*Workflow, error) { return workflow, nil } -func getEnvironments(ctx context.Context, OrgId string) ([]Environment, error) { +func getEnvironments(ctx context.Context, orgId string) ([]Environment, error) { var environments []Environment - q := datastore.NewQuery("Environments").Filter("org_id =", OrgId) + q := datastore.NewQuery("Environments").Filter("org_id =", orgId) _, err := dbclient.GetAll(ctx, q, &environments) if err != nil { @@ -3513,9 +3516,9 @@ func getEnvironments(ctx context.Context, OrgId string) ([]Environment, error) { return environments, nil } -func getAllWorkflows(ctx context.Context) ([]Workflow, error) { +func getAllWorkflows(ctx context.Context, orgId string) ([]Workflow, error) { var allworkflows []Workflow - q := datastore.NewQuery("workflow") + q := datastore.NewQuery("workflow").Filter("org_id = ", orgId) _, err := dbclient.GetAll(ctx, q, &allworkflows) if err != nil { @@ -3656,7 +3659,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { private = true } - q := datastore.NewQuery("workflow") + q := datastore.NewQuery("workflow").Filter("org_id = ", user.ActiveOrg.Id) var workflows []Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { @@ -3833,8 +3836,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - // FIXME - need to be logged in? - _, userErr := handleApiAuthentication(resp, request) + user, userErr := handleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -3903,6 +3905,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { } } + appAuth.OrgId = user.ActiveOrg.Id err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting up app auth %s: %s", appAuth.Id, err) @@ -5467,9 +5470,13 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } -func getAllSchedules(ctx context.Context) ([]ScheduleOld, error) { +func getAllSchedules(ctx context.Context, orgId string) ([]ScheduleOld, error) { var schedules []ScheduleOld - q := datastore.NewQuery("schedules") + + q := datastore.NewQuery("schedules").Filter("org = ", orgId) + if orgId == "ALL" { + q = datastore.NewQuery("schedules") + } _, err := dbclient.GetAll(ctx, q, &schedules) if err != nil { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 446ab4df..5aec5349 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -39,6 +39,7 @@ var globalUrl = window.location.origin // CORS used for testing purposes. Should only happen with specific port and http if (window.location.protocol == "http:" && window.location.port === "3000") { globalUrl = "http://localhost:5001" + //globalUrl = "http://localhost:5002" } const theme = createMuiTheme({ diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 34141a0e..d3deb492 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -157,7 +157,7 @@ const Admin = (props) => { alert.error("Failed stopping schedule") } else { getAppAuthentication() - alert.success("Successfully stopped schedule!") + alert.success("Successfully deleted authentication!") } }), ) @@ -1184,7 +1184,7 @@ const Admin = (props) => { color="primary" onClick={() => deleteSchedule(schedule)} > - Delete + Stop schedule @@ -1443,7 +1443,7 @@ const Admin = (props) => { - + Date: Thu, 5 Nov 2020 11:49:10 +0100 Subject: [PATCH 02/18] Fixed edit password bug --- backend/go-app/main.go | 92 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index c73bcd72..7792d5c1 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2118,7 +2118,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { } if len(foundUser.Id) == 0 { - log.Printf("Something went wrong in password reset", err) + log.Printf("Something went wrong in password reset: couldn't find user.") resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return @@ -6486,21 +6486,96 @@ type CloudSyncJob struct { Created string `json:"created" datastore:"created"` } +func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executionArgument string) error { + ctx := context.Background() + // 1. Get the workflow + // 2. Execute it with the data + workflow, err := getWorkflow(ctx, workflowId) + if err != nil { + return err + } + // FIXME: Handle auth + _ = workflow + + type execStruct struct { + ExecutionSource string `json:"execution_source"` + ExecutionArgument string `json:"execution_argument"` + Start string `json:"start,omitempty"` + } + + parsedArgument := strings.Replace(string(executionArgument), "\"", "\\\"", -1) + newExec := execStruct{ + ExecutionSource: executionSource, + ExecutionArgument: parsedArgument, + } + + //bodyWrapper := fmt.Sprintf(`{"execution_source": "%s", "execution_argument": "%s"}`, executionSource, parsedArgument) + if len(startNode) > 0 { + newExec.Start = startNode + } + + b, err := json.Marshal(newExec) + if err != nil { + log.Printf("Failed marshal") + return err + } + + log.Println(string(b)) + newRequest := &http.Request{ + Method: "POST", + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + + _, _, err = handleExecution(workflowId, Workflow{}, newRequest) + return err +} + func handleCloudJob(job CloudSyncJob) error { log.Printf("Handle job with type %s and action %s", job.Type, job.Action) if job.Type == "webhook" { if job.Action == "execute" { log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from cloud hook: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud hook: %s", err) + } + } + + } else if job.Type == "schedule" { + if job.Action == "execute" { + log.Printf("Should handle schedule for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "schedule", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from cloud schedule: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud schedule") + } + } + } else if job.Type == "email_trigger" { + if job.Action == "execute" { + log.Printf("Should handle email for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "email_trigger", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from email trigger: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud email trigger") + } } } else if job.Type == "user_input" { - log.Printf("Should handle user_input for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) - } else if job.Type == "schedule" { - log.Printf("Should handle schedule for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) - } else if job.Type == "email" { - log.Printf("Should handle email for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + if job.Action == "execute" { + log.Printf("Should handle user_input for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "user_input", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from cloud user_input: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud user_input") + } + } } else { - log.Printf("No handler for type %s", job.Type) + log.Printf("No handler for type %s and action %s", job.Type, job.Action) } return nil @@ -6984,7 +7059,7 @@ func runInit(ctx context.Context) { job := func() { err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("Failed request with remote org setup: err") + log.Printf("Failed request with remote org setup: %s", err) } } @@ -7261,6 +7336,7 @@ func handleStopCloudSync(syncUrl string, org Org) error { } } + // FIXME: This doesn't work? if value, exists := scheduledOrgs[org.Id]; exists { // Looks like this does the trick? Hurr log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) From 4d0f9f5bc483515369cc0b14df5be1b4cfe2f60f Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 5 Nov 2020 12:29:39 +0100 Subject: [PATCH 03/18] #198: Fixed bug with optional queries --- backend/go-app/codegen.go | 86 +++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 609acfc9..a4aa2ec6 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -397,7 +397,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet verifyAddin, ) - if strings.Contains(functionname, "api_dumps_delete") { + if strings.Contains(functionname, "search") { log.Println(data) log.Printf("Queries: %s", queryString) } @@ -1031,6 +1031,12 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1059,12 +1065,6 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1165,6 +1165,12 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1193,11 +1199,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1299,6 +1300,12 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1326,13 +1333,6 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1433,6 +1433,12 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1461,12 +1467,6 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1566,6 +1566,12 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1593,12 +1599,6 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1699,6 +1699,12 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1726,12 +1732,6 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1832,6 +1832,12 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1860,12 +1866,6 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } From 7b51b9ef22e072e1456844b550f9a8537927d9d6 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 5 Nov 2020 17:11:50 +0100 Subject: [PATCH 04/18] Got first working cloud sync trigger fully running --- backend/app_sdk/app_base.py | 8 +- backend/go-app/codegen.go | 10 +- backend/go-app/main.go | 131 +++++++++++++++++++---- backend/go-app/walkoff.go | 40 ++++++- backend/tests/hooks.sh | 12 +-- frontend/src/App.jsx | 4 +- frontend/src/components/AlertTemplate.js | 7 +- frontend/src/views/AngularWorkflow.jsx | 82 +++++++++++--- frontend/src/views/AppCreator.jsx | 1 + 9 files changed, 232 insertions(+), 63 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 1492305a..253bf8e2 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -478,7 +478,7 @@ class AppBase: #Actionname: Start_node - print(f"Actionname: {actionname}") + print(f"Actionname: {actionname_lower}") # 1. Find the action baseresult = "" @@ -556,14 +556,14 @@ class AppBase: return ""+appendresult, False if len(parsersplit) == 1: - return baseresult+appendresult, False + return str(baseresult)+str(appendresult), False baseresult = baseresult.replace("\'", "\"") basejson = {} try: basejson = json.loads(baseresult) except json.decoder.JSONDecodeError as e: - return baseresult+appendresult, False + return str(baseresult)+str(appendresult), False data, is_loop = recurse_json(basejson, parsersplit[1:]) parseditem = data @@ -577,7 +577,7 @@ class AppBase: print("SET DATA WRAPPER TO %s!" % parsersplit[-1]) parseditem = "${%s%s}$" % (parsersplit[-1], json.dumps(data)) - return parseditem+appendresult, is_loop + return str(parseditem)+str(appendresult), is_loop # Parses parameters sent to it and returns whether it did it successfully with the values found def parse_params(action, fullexecution, parameter): diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index a4aa2ec6..2099affb 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -397,10 +397,12 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet verifyAddin, ) - if strings.Contains(functionname, "search") { - log.Println(data) - log.Printf("Queries: %s", queryString) - } + /* + if strings.Contains(functionname, "search") { + log.Println(data) + log.Printf("Queries: %s", queryString) + } + */ //log.Printf(data) return functionname, data diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 7792d5c1..93cb03be 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -381,16 +381,17 @@ type HookAction struct { } type Hook struct { - Id string `json:"id" datastore:"id"` - Start string `json:"start" datastore:"start"` - Info Info `json:"info" datastore:"info"` - Actions []HookAction `json:"actions" datastore:"actions,noindex"` - Type string `json:"type" datastore:"type"` - Owner string `json:"owner" datastore:"owner"` - Status string `json:"status" datastore:"status"` - Workflows []string `json:"workflows" datastore:"workflows"` - Running bool `json:"running" datastore:"running"` - OrgId string `json:"org_id" datastore:"org_id"` + Id string `json:"id" datastore:"id"` + Start string `json:"start" datastore:"start"` + Info Info `json:"info" datastore:"info"` + Actions []HookAction `json:"actions" datastore:"actions,noindex"` + Type string `json:"type" datastore:"type"` + Owner string `json:"owner" datastore:"owner"` + Status string `json:"status" datastore:"status"` + Workflows []string `json:"workflows" datastore:"workflows"` + Running bool `json:"running" datastore:"running"` + OrgId string `json:"org_id" datastore:"org_id"` + Environment string `json:"environment" datastore:"environment"` } func createFileFromFile(ctx context.Context, bucket *storage.BucketHandle, remotePath, localPath string) error { @@ -3420,6 +3421,10 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } + if hook.Environment == "cloud" { + log.Printf("This should trigger in the cloud. Duplicate action allowed onprem.") + } + for _, item := range hook.Workflows { log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) workflow := Workflow{ @@ -3471,6 +3476,51 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } } +func executeCloudAction(action CloudSyncJob, apikey string) error { + data, err := json.Marshal(action) + if err != nil { + log.Printf("Failed cloud webhook action marshalling", err) + return err + } + + client := &http.Client{} + syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync/handle_action", syncUrl) + req, err := http.NewRequest( + "POST", + syncUrl, + bytes.NewBuffer(data), + ) + + req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey)) + newresp, err := client.Do(req) + if err != nil { + return err + } + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + return err + } + + type Result struct { + Success bool `json:"success"` + Reason string `json:"reason"` + } + + log.Printf("Data: %s", string(respBody)) + responseData := Result{} + err = json.Unmarshal(respBody, &responseData) + if err != nil { + return err + } + + if !responseData.Success { + return errors.New(fmt.Sprintf("Error from Shuffler: %s", responseData.Reason)) + } + + return nil +} + // Starts a new webhook func handleNewHook(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -3493,6 +3543,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Name string `json:"name"` Workflow string `json:"workflow"` Start string `json:"start"` + Environment string `json:"environment"` } body, err := ioutil.ReadAll(request.Body) @@ -3551,6 +3602,37 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } + // Let remote endpoint handle access checks (shuffler.io) + currentUrl := fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId) + if requestdata.Environment == "cloud" { + log.Printf("[INFO] Should START a cloud webhook for url %s", currentUrl) + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + action := CloudSyncJob{ + Type: "webhook", + Action: "start", + OrgId: org.Id, + PrimaryItemId: newId, + SecondaryItem: requestdata.Start, + ThirdItem: requestdata.Workflow, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action START execution", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully set up cloud action schedule") + } + } + hook := Hook{ Id: newId, Start: requestdata.Start, @@ -3558,7 +3640,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Info: Info{ Name: requestdata.Name, Description: requestdata.Description, - Url: fmt.Sprintf("https://shuffler.io/functions/webhooks/webhook_%s", newId), + Url: fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId), }, Type: "webhook", Owner: user.Username, @@ -3571,8 +3653,9 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Field: "", }, }, - Running: false, - OrgId: user.ActiveOrg.Id, + Running: false, + OrgId: user.ActiveOrg.Id, + Environment: requestdata.Environment, } hook.Status = "running" @@ -3587,10 +3670,10 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { err = increaseStatisticsField(ctx, "total_workflow_triggers", requestdata.Workflow, 1) if err != nil { - log.Printf("Failed to increase total workflows: %s", err) + log.Printf("[INFO] Failed to increase total workflows: %s", err) } - log.Println("Set up a new hook") + log.Printf("Set up a new hook with ID %s and environment %s", newId, hook.Environment) resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) } @@ -6476,6 +6559,7 @@ func handleAppHotload(location string, forceUpdate bool) error { // Secondary = something to specify what inside workflow to execute // Third = Some data to add to it type CloudSyncJob struct { + Id string `json:"id" datastore:"id"` Type string `json:"type" datastore:"type"` Action string `json:"action" datastore:"action"` OrgId string `json:"org_id" datastore:"org_id"` @@ -6534,12 +6618,12 @@ func handleCloudJob(job CloudSyncJob) error { log.Printf("Handle job with type %s and action %s", job.Type, job.Action) if job.Type == "webhook" { if job.Action == "execute" { - log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) if err != nil { log.Printf("Failed executing workflow from cloud hook: %s", err) } else { - log.Printf("Successfully executed workflow from cloud hook: %s", err) + log.Printf("Successfully executed workflow from cloud hook!") } } @@ -6589,7 +6673,6 @@ func remoteOrgJobController(org Org, body []byte) error { Jobs []CloudSyncJob `json:"jobs"` } - log.Printf("Remote JOB ret: %s", string(body)) responseData := retStruct{} err := json.Unmarshal(body, &responseData) if err != nil { @@ -6633,7 +6716,11 @@ func remoteOrgJobController(org Org, body []byte) error { return errors.New("[ERROR] Remote job handler issues.") } - log.Printf("Got job with reason %s and %d jobs", responseData.Reason, len(responseData.Jobs)) + if len(responseData.Jobs) > 0 { + log.Printf("Remote JOB ret: %s", string(body)) + log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs)) + } + for _, job := range responseData.Jobs { err = handleCloudJob(job) if err != nil { @@ -6666,6 +6753,8 @@ func remoteOrgJobHandler(org Org, interval int) error { return err } + log.Printf("Data: %s", respBody) + err = remoteOrgJobController(org, respBody) if err != nil { log.Printf("Failed job controller run: %s", err) @@ -7059,7 +7148,7 @@ func runInit(ctx context.Context) { job := func() { err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("Failed request with remote org setup: %s", err) + log.Printf("Failed request with remote org setup (2): %s", err) } } @@ -7550,7 +7639,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { job := func() { err := remoteOrgJobHandler(*org, interval) if err != nil { - log.Printf("Failed request with remote org setup: err") + log.Printf("Failed request with remote org setup (1): %s", err) } } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 35f9e329..ad742389 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -73,6 +73,10 @@ type SyncFeatures struct { Schedules SyncData `json:"schedules" datastore:"schedules"` Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` Authentication SyncData `json:"authentication" datastore:"authentication"` + Webhook SyncData `json:"webhook" datastore:"webhook"` + Schedule SyncData `json:"schedule" datastore:"schedule"` + UserInput SyncData `json:"user_input" datastore:"user_input"` + EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` } type SyncData struct { @@ -2657,7 +2661,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !startFound { log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) - return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf("Workflow start node %s doesn't exist. Exiting!", workflowExecution.Start)) + return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) } // Verification for execution environments @@ -4985,7 +4989,7 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, } if appCounter > 0 { - log.Printf("Preloaded %d OpenApi apps in %s!", appCounter, extra) + log.Printf("Preloaded %d OpenApi apps in folder %s!", appCounter, extra) } return nil @@ -5662,7 +5666,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { return } - if user.Id != hook.Owner && user.Role != "admin" { + if user.Id != hook.Owner && user.Role != "admin" && user.ActiveOrg.Id != hook.OrgId { log.Printf("Wrong user (%s) for workflow %s", user.Username, hook.Id) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) @@ -5685,6 +5689,36 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { return } + log.Printf("Hook: %#v", hook) + if hook.Environment == "cloud" { + log.Printf("[INFO] Should STOP cloud webhook https://shuffler.io/api/v1/hooks/webhook_%s", hook.Id) + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + action := CloudSyncJob{ + Type: "webhook", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: hook.Id, + } + + if len(hook.Workflows) > 0 { + action.SecondaryItem = hook.Workflows[0] + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action STOP execution", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + } + // This is here to force stop and remove the old webhook //image := "webhook" //err = removeWebhookFunction(ctx, fileId) diff --git a/backend/tests/hooks.sh b/backend/tests/hooks.sh index 4689fed9..738111af 100644 --- a/backend/tests/hooks.sh +++ b/backend/tests/hooks.sh @@ -23,14 +23,4 @@ #curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_3ceff795-ce9a-43a2-a2f5-d4401a6e772d" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' #curl POST "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_be4dbb0a-d396-4544-bc36-e57d1bdb2e40" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' -vvv -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5002/api/v1/hooks/webhook_f22b5e54-e55d-48e5-a1d1-f40453513fd3" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5aec5349..aed81d56 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,7 +30,7 @@ import SettingsPage from "./views/SettingsPage"; import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; -import AlertTemplate from "react-alert-template-basic"; +import AlertTemplate from "./components/AlertTemplate"; import { positions, Provider } from "react-alert"; // Production - backend proxy forwarding in nginx @@ -118,7 +118,7 @@ const App = (message, props) => { const options = { timeout: 5000, - position: positions.BOTTOM_CENTER + position: positions.BOTTOM_RIGHT }; const includedData = window.location.pathname === "/home" || window.location.pathname === "/features" ? diff --git a/frontend/src/components/AlertTemplate.js b/frontend/src/components/AlertTemplate.js index bf5066dd..e86da840 100644 --- a/frontend/src/components/AlertTemplate.js +++ b/frontend/src/components/AlertTemplate.js @@ -7,7 +7,7 @@ import CloseIcon from './icons/CloseIcon' const alertStyle = { backgroundColor: '#151515', color: 'white', - padding: '10px', + padding: 15, textTransform: 'uppercase', borderRadius: '3px', display: 'flex', @@ -15,8 +15,9 @@ const alertStyle = { alignItems: 'center', boxShadow: '0px 2px 2px 2px rgba(0, 0, 0, 0.03)', fontFamily: 'Arial', - width: '300px', - boxSizing: 'border-box' + width: 400, + boxSizing: 'border-box', + zIndex: 10001, } const buttonStyle = { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index fa480e33..eec5bf38 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -190,6 +190,7 @@ const AngularWorkflow = (props) => { const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0) const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false + const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' useBeforeunload(() => { @@ -1907,7 +1908,7 @@ const AngularWorkflow = (props) => { "large_image": 'data:image/png;base64,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', "is_valid": true, "label": "Webhook", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "onprem", "description": "Simple HTTP webhook", "long_description": "Execute a workflow with an unauthicated POST request", }, @@ -1944,7 +1945,7 @@ const AngularWorkflow = (props) => { "description": "Add your email provider", "trigger_type": "EMAIL", "errors": null, - "is_valid": cloudSyncEnabled, + "is_valid": true ? false : cloudSyncEnabled, "label": "Email", "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/AAAsIAGQAZAEBEQD/xAAeAAABAwUBAQAAAAAAAAAAAAAAAQgJAgQFBwoGA//EAEoQAAECBAMEBwMDEQgDAAAAAAECAwAEBREGBxIIITFRCRMiMkFSYRRicSNCQxUWGBk2U1dYc3WBlJWzwdLTJDNjZXKDkeEmgqH/2gAIAQEAAD8Ak8JKipSlBwuDSpSeDw8qeRguQQrUAQNAV4JH3s+sA7OnT8n1fcv9Bfzc7wWAAToIAOsJ8Uq++H0gI1XSUlYWdSkji6fMnkBAdS9SidesWUpI3OjyjkYXtXCgbEDSFW3JT5D6+sIOzp0/J9X3NX0F/NzvBYABOggA6wnxSr74fSAjVdJSVhZ1KSOLp8yeQEBJUSVKCysaVKHB0eVPIwXIIVqAIGgK8Ej72fWFS640kNtzjcukcGli6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EaB2k9tzInZilVyuMq+up4iUjWxh+mFLs8s23dbv0stnmsgnwBiNPOHpddozHExMSmWsrSsA0tZIaMs0JudCfV50FKf/AFQPjDYsT7S+0LjJ8zGJc68aTqiSrSqtPoQD6ISoJH6BGFkM5c3qW8Jim5qYvlXArUFNVuZSb89y43Tlv0ju15ltMtKZzUmsRSbZGuSxC0mebcHIrV8r/wALEP02c+l2ywzAmZXDGeND+saqvEIRVWXFP0x1Z3AOE9thPx1JHioQ/un1Kn1eQZqtLn2ZuSmkJdamZVwOIWlQuktKTuUg8xFybgnUACB2gjgkc0e9BvuAAm9rgHulPM+/CpDik3bal1p8FPd8/GEtp7Ojq+r36OPUe96wWv2dF79vR5/8T/qI/ukM6RMZMGcyWyUqDMzjZ5vRWKwmy26UlQ3IQOBmLHx3IFibncIeqvWKtiCqTVbrtSmahUJ11T0zNTLqnHXnFG5UpSiSSeZi0ggggh2GxRt8Y92X69K4cr83N1zLuadCZumLXrcp4Ue0/KXPZPiW+6r0O+JxsF4zwvmFhSl42wXV5eo0Sqy6ZySmWFakJbUO/wDHiCk7wQQd4jN2v2dF79vR5v8AE/6g6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ibpt3bTrOzBkbPYhpb7f11V5aqZh9le8iZUm65i3i20ntcirQPGIA6nU6jWqlNVirzr05PTzy5iZmHllTjrqyVKWoneSSSSYtoIIIIIIkI6J/axmsA4+Rs84yqZ+tvFb5VQ1vL7EjVCNyN/Bt4C1vOEn5xiYndYghVr3IHeKuY9yKVJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQiDnpWM5ZnMrafn8HS04pykZfy6KMw2D2BNEByZUPXWQj/aEM0h3uwvk5PYmoeLM4HcnqHmth/CU1KytdwrNy5M+uUdQtZmZBYIu83oN2z3wbcbWk7ym2Zej6zuwXJ49y5yXwbUqXNgpUPZlpelnh32XmyrU24k7ik7/0WMey+wI2OPxesJfqyv5oPsCNjj8XrCX6sr+aD7AjY4/F6wl+rq/mhs2b2SGzXjPGc3s9bKuzngiq4zZGjEWJ3pRTlKwkyrcVOKCrOzVr6GRex73AiIe65TvqRWqhSet632Kadl9enTq0LKb28L24R86ZUp6jVKUrFMmVy85IvtzMu8g2U24hQUlQPMEAx0h7O+abGdWR+DM0mnAF16lMuzRTxamgNDzQHIOJWI2IpxDZ0Lm3ZdQ4tti6U/CEdeDaHJhxYWAklahwdAF9KeRjmXzRxJMYxzLxXiyacUt2sVqdnlFRuflHlq/jHmIlv6D37is1T/mlM/cvQ5zNnZ7xtl/jWc2htlFUtIYrfs7iXCLy+rpeLGk7zcDczN2vpdFrnvcSTsrIPaHwNtBYcmKlhsv02t0h4ydfw7UE9XUKPOJJC2Xmzv3KBAWNyrbvEDaDjjbTa3XVpQhAKlKUbBIHEk+ENLxdnDj/AGr8T1LJ7Zgra6NgymPmSxhmU0LhB+kkaV4OPkGynu6gG4PAlwGUuT2AMjsDy2BMuqGin06XBcdWTrfm3j3333D2nHFHeVH/AOCwjmnxr92Ve/Oc1+9VGGibDogMVP1vZWmKI+4b4dxJOybS1G4S06ht7QPipxf/ADD4kurbGhE21LpHBtwXUn4xbVNpb9OnGAAFrl3EkI4JukgFHrHMFWpdyUrE/KvAhxmZdbUDxBCyDFnEuHQej/wjNU/5rTP3L0PQ2hs1cR4f+pOUWU/VTGZeOtbFK1jW3SZNO6YqkwPBtlJ7IPfcKUi++NdYh2H5LB1CoWLtnXFD2Fs1sLS6rV+ZUXG8SqWouPtVVP0yXnCo6+8gqFtwAGAbkdpzbFcTgXNLB1Qyay9pBEri1iXm9VQxPNo/vJeVdT/dyJ3XcG9YNgTvt6TFODKZsY4nlc2MsaEmRypn25em45oMi2eqpiUANsVllA8gsiYtvUiyzcpJhz8rOylSkGqjITTUzKzTKXmHmlhSHG1C6VJI3EEEEGOXnGv3ZV785zX71UYaJjehfk3mdn/GM4oHRM4sWEBfcsiUZ1Eeu+JBUhxSbttS60+Cnu+fjCAaDbR1fV9rRx6n3vW8c5+2Bl1MZV7TGYmDXmlIaZrkxNyhIsFy0wrr2lD00OJjT0SfdE5mphvJnIrOXHmJutdalatSmZSSlxqmKhNuNOpYlWU8VOOLISAOdzuBiQLZ5yrxJQTVs382Q0/mVjrQ9VAk6m6RJp3y9Llz4NtA9ojvuFSjfdG54N8fGekZOpyUxTajKtTMrNNLYfYdSFIdbULKSpJ3EEEgiG4ZXz07s0Zis7OuJpp1zAmJFvP5cVSYWSJVYut2hurPzkC62Ce83dHFFogGxr92Ve/Oc1+9VGGiezo0MupnLzY/wezPy5bm8RqmMROsqFiUvr+SWf8AaQ2besOl6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ii76Y7Z4mJgUHaSw7IqWhlCKHiLQN6RcmWmP9Nypsn8mIizj3GWGdOY2T9Xka1gSuJlH6bO/VKWbflm5hlubDam0v9U4lSC4lClBKiLp1G1iY3v8AbSdtr8LTP7Ekf6UL9tJ22vwss/sOR/pQfbSdtr8LLP7Dkf6UH20nba/C0z+w5H+lHmMxOkB2qc1MOKwrjjMNmfkPaGZxrTSZRl1iYZWFtPNOobC21pULhSSDx8DDe5uamJ6aenZt1Tr8w4p11auKlqNyT8SY2ZszZH1raIzqw1ldSG1hqozSXKlMAHTKyLZCn3VHwsi4HvKSPGOjSj0im0CjyVBpMqJen06XalZZhG7Q22kJQE+4AAIulJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQjB45wVhrMbB9YwNjGnIn6JW5VyQnWVjihYtoTysbEKHAgGOf7a72U8abKeZszhStMOzVAnVreoNXCfk5uXvuSojcHUAgLTz3jcRGi4IIIIIuqVSqnXKnK0ajSD89PzzyJeWlpdsrcecUbJQlI3kkkAAROd0eGxqjZiy7XiLF8sy5j/FjaFVEiyhJMDeiSB9D2lkbiqw4JEO58CrUQAdJV4pPkHu+sIpxDZ0Lm3ZdQ4tti6U/CFJKiVKUFle5Sk8HR5U8jBcghWoAgaArwSPIfe9Y8PnJkvl1nzgScy7zMoDVQpMyLtlXZekHfmvNucULHgR8DcEiIZtqro1s58gZucxFg6QmsbYJQVOonpFkqnJNrw9pYTcgAfSJuk8Tp4Qz9SSklKgQQbEHwgggjYeTOz9m7n/iFGHMq8Fz1YdCgJiZSjRKSiT8955XYQB6m58AYmN2LejtwJsyoYxri52XxVmC43unA3/ZpAEb0ygVvv4F09ojgEgm7wSSq5Kgsr3KUODo8qeRguQQrUAQNAV4JHkPvesKl1bY0Im2pdI4NuC6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EG4i4KiCbAnvE8j7kaHzf2Hdl/O1+YqONcraexU3jd6p0i8jNFfPU1ZLnxWlUNjxL0LOTs6+tzC+bWLKSkHUWpmXl5xKR4BJAbJjD0/oTcEoeH1Uz5rbzfe0sUZlolH+pTigFelo3Nlr0UuyZgSYZn6vQ6zjKaQQpr6uz3yBI462WQhNvRVxDscMYVwvgujMYfwfh+n0Wly/ZZlJCVQw2k8tCABp9YypsL6iQAe0U8Unkj3YDcE6gAQO0EcEjmj3oN9wAE3tcA90p5n34VIcUm7bUutPgp7vn4wOpS05MNtiyZdAW0PKo+MASkuIbIulbPXKHNfOEa+V9m6zf7Vq633rcIpSoqbQ6T2lvdQo80coVxRbRMLRuVLuBts+VJ4iKnEhtb6ECwl0BxseVR8YAlJcQ2RdK2euUOa+cI18r7N1m/wBq1db71uEUpUVNodJ7S3uoUeaOUK4otomFo3Kl3A22fKk8RFTiQ2t9CBYS6A42PKo+MASkuIbIulbPXKHNfOPtKSkvNy6JiYaC3F71KJO+P//Z', @@ -2063,9 +2064,9 @@ const AngularWorkflow = (props) => { position: newposition, } - if (data.trigger_type === "WEBHOOK") { - newAppData.status = "running" - } + //if (data.trigger_type === "WEBHOOK") { + // newAppData.status = "running" + //} // Can all the data be in here? hmm const nodeToBeAdded = { @@ -2110,10 +2111,10 @@ const AngularWorkflow = (props) => { } setWorkflow(workflow) - if (data.trigger_type === "WEBHOOK") { - newWebhook(newAppData) - saveWorkflow(workflow) - } + //if (data.trigger_type === "WEBHOOK") { + // newWebhook(newAppData) + // saveWorkflow(workflow) + //} } } } @@ -4526,6 +4527,55 @@ const AngularWorkflow = (props) => { placeholder={selectedTrigger.label} onChange={selectedTriggerChange} /> + {showEnvironment ? +
+ + Environment + + +
+ : null}
@@ -4728,6 +4778,7 @@ const AngularWorkflow = (props) => { "id": trigger.id, "workflow": workflow.id, "start": startNode, + "environment": trigger.environment, } fetch(globalUrl+"/api/v1/hooks/new", { @@ -4762,7 +4813,7 @@ const AngularWorkflow = (props) => { alert.info("Stopping webhook") fetch(globalUrl+"/api/v1/hooks/"+trigger.id+"/delete", { - method: 'DELETE', + method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -4776,14 +4827,15 @@ const AngularWorkflow = (props) => { return response.json() }) - .then((responseJson) => { + .then((responseJson) => { + workflow.triggers[triggerindex].status = "stopped" + trigger.status = "stopped" + setWorkflow(workflow) + setSelectedTrigger(trigger) + if (responseJson.success) { //alert.success("Successfully stopped webhook") // Set the status - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setWorkflow(workflow) - setSelectedTrigger(trigger) saveWorkflow(workflow) } else { alert.error("Failed stopping webhook: "+responseJson.reason) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 4d5098bd..c2240024 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -884,6 +884,7 @@ const AppCreator = (props) => { } }) .catch(error => { + setAppBuilding(false) setErrorCode(error.toString()) alert.error(error.toString()) }); From d67bdc7730fb2adb989681c6cd685e51fd33e747 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 5 Nov 2020 17:32:45 +0100 Subject: [PATCH 05/18] Updated the alert system --- frontend/src/App.jsx | 2 +- frontend/src/components/AlertTemplate.js | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aed81d56..8e57b486 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -118,7 +118,7 @@ const App = (message, props) => { const options = { timeout: 5000, - position: positions.BOTTOM_RIGHT + position: positions.TOP_CENTER, }; const includedData = window.location.pathname === "/home" || window.location.pathname === "/features" ? diff --git a/frontend/src/components/AlertTemplate.js b/frontend/src/components/AlertTemplate.js index e86da840..a476afc7 100644 --- a/frontend/src/components/AlertTemplate.js +++ b/frontend/src/components/AlertTemplate.js @@ -1,11 +1,12 @@ import React from 'react' -import InfoIcon from './icons/InfoIcon' -import SuccessIcon from './icons/SuccessIcon' -import ErrorIcon from './icons/ErrorIcon' -import CloseIcon from './icons/CloseIcon' +import InfoIcon from '@material-ui/icons/Info'; +import CheckIcon from '@material-ui/icons/Check'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; +import CloseIcon from '@material-ui/icons/Close'; +import Typography from '@material-ui/core/Typography'; const alertStyle = { - backgroundColor: '#151515', + backgroundColor: 'rgba(0,0,0,0.9)', color: 'white', padding: 15, textTransform: 'uppercase', @@ -14,10 +15,9 @@ const alertStyle = { justifyContent: 'space-between', alignItems: 'center', boxShadow: '0px 2px 2px 2px rgba(0, 0, 0, 0.03)', - fontFamily: 'Arial', width: 400, boxSizing: 'border-box', - zIndex: 10001, + zIndex: 100001, } const buttonStyle = { @@ -31,10 +31,10 @@ const buttonStyle = { const AlertTemplate = ({ message, options, style, close }) => { return (
- {options.type === 'info' && } - {options.type === 'success' && } - {options.type === 'error' && } - {message} + {options.type === 'info' && } + {options.type === 'success' && } + {options.type === 'error' && } + {message} From 8232dbf2e1ccd0529f3e7b7b32c4d62710d1c8f2 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 6 Nov 2020 16:42:21 +0100 Subject: [PATCH 06/18] Added basic organization edit page --- backend/go-app/main.go | 76 ++++++- backend/go-app/walkoff.go | 15 +- frontend/src/App.jsx | 2 +- frontend/src/views/Admin.jsx | 345 ++++++++++++++++++++++++++++--- functions/onprem/worker/build.sh | 2 +- 5 files changed, 394 insertions(+), 46 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 93cb03be..413d3ec8 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2298,6 +2298,70 @@ func handleGetEnvironments(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } +func handleGetOrg(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in set new workflowhandler: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + ctx := context.Background() + org, err := getOrg(ctx, fileId) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed getting org users"}`)) + return + } + + //FIXME : cleanup org before marshal + userFound := false + for _, foundUser := range org.Users { + if foundUser.Id == user.Id { + userFound = true + break + } + } + + if !userFound { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Use doesn't have access to org"}`)) + return + } + + org.Users = []User{} + org.SyncConfig.Apikey = "" + newjson, err := json.Marshal(org) + if err != nil { + log.Printf("Failed unmarshal of org: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking"}`))) + return + } + + resp.WriteHeader(200) + resp.Write(newjson) +} + func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -2312,7 +2376,7 @@ func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { return } - if user.Role != "admin" { + if user.Role != "global_admin" { resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Not admin"}`)) return @@ -3507,7 +3571,7 @@ func executeCloudAction(action CloudSyncJob, apikey string) error { Reason string `json:"reason"` } - log.Printf("Data: %s", string(respBody)) + //log.Printf("Data: %s", string(respBody)) responseData := Result{} err = json.Unmarshal(respBody, &responseData) if err != nil { @@ -3554,7 +3618,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Data: %s", string(body)) + //log.Printf("Data: %s", string(body)) ctx := context.Background() var requestdata requestData @@ -6743,7 +6807,7 @@ func remoteOrgJobHandler(org Org, interval int) error { req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey)) newresp, err := client.Do(req) if err != nil { - log.Printf("Failed request in org sync: %s", err) + //log.Printf("Failed request in org sync: %s", err) return err } @@ -6753,7 +6817,7 @@ func remoteOrgJobHandler(org Org, interval int) error { return err } - log.Printf("Data: %s", respBody) + //log.Printf("Data: %s", respBody) err = remoteOrgJobController(org, respBody) if err != nil { @@ -7958,6 +8022,8 @@ func initHandlers() { // NEW for 0.8.0 r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") // Important for email, IDS etc. Create this by: // PS: For cloud, this has to use cloud storage. diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index ad742389..6ce5a77b 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -68,19 +68,22 @@ type ExecutionRequest struct { } type SyncFeatures struct { + Webhook SyncData `json:"webhook" datastore:"webhook"` + Schedule SyncData `json:"schedule" datastore:"schedule"` + UserInput SyncData `json:"user_input" datastore:"user_input"` + EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` Apps SyncData `json:"apps" datastore:"apps"` Workflows SyncData `json:"workflows" datastore:"workflows"` Schedules SyncData `json:"schedules" datastore:"schedules"` Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` Authentication SyncData `json:"authentication" datastore:"authentication"` - Webhook SyncData `json:"webhook" datastore:"webhook"` - Schedule SyncData `json:"schedule" datastore:"schedule"` - UserInput SyncData `json:"user_input" datastore:"user_input"` - EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` } type SyncData struct { - Active bool `json:"active" datastore:"active"` + Active bool `json:"active" datastore:"active"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Limit int64 `json:"limit" datastore:"limit"` } type SyncConfig struct { @@ -91,6 +94,8 @@ type SyncConfig struct { // Role is just used for feedback for a user type Org struct { Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Image string `json:"image" datastore:"image"` Id string `json:"id" datastore:"id"` Org string `json:"org" datastore:"org"` Users []User `json:"users" datastore:"users"` diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8e57b486..16c2ca17 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -118,7 +118,7 @@ const App = (message, props) => { const options = { timeout: 5000, - position: positions.TOP_CENTER, + position: positions.BOTTOM_LEFT, }; const includedData = window.location.pathname === "/home" || window.location.pathname === "/features" ? diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index d3deb492..817a20e2 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1,7 +1,10 @@ import React, { useEffect} from 'react'; +import { makeStyles } from '@material-ui/styles'; import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; +import Card from '@material-ui/core/Card'; +import Tooltip from '@material-ui/core/Tooltip'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Typography from '@material-ui/core/Typography'; import Switch from '@material-ui/core/Switch'; @@ -20,6 +23,7 @@ import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import IconButton from '@material-ui/core/IconButton'; import Avatar from '@material-ui/core/Avatar'; +import Zoom from '@material-ui/core/Zoom'; import { useAlert } from "react-alert"; @@ -41,19 +45,30 @@ import ScheduleIcon from '@material-ui/icons/Schedule'; import CloudIcon from '@material-ui/icons/Cloud'; import BusinessIcon from '@material-ui/icons/Business'; -const Admin = (props) => { - const { globalUrl } = props; +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important" + }, +}) +const Admin = (props) => { + const { globalUrl, userdata } = props; + + var upload = "" const theme = useTheme(); + const classes = useStyles(); const [firstRequest, setFirstRequest] = React.useState(true); const [modalUser, setModalUser] = React.useState({}); const [modalOpen, setModalOpen] = React.useState(false); + const [file, setFile] = React.useState(""); + const [fileBase64, setFileBase64] = React.useState(""); const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false); const [cloudSyncApikey, setCloudSyncApikey] = React.useState(""); const [loading, setLoading] = React.useState(false); const [selectedOrganization, setSelectedOrganization] = React.useState({}); + const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [loginInfo, setLoginInfo] = React.useState(""); const [curTab, setCurTab] = React.useState(0); const [users, setUsers] = React.useState([]); @@ -244,6 +259,8 @@ const Admin = (props) => { selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync setSelectedOrganization(selectedOrganization) setCloudSyncApikey("") + + handleGetOrg(userdata.active_org.id) } }) .catch(error => { @@ -312,6 +329,50 @@ const Admin = (props) => { }); } + const handleGetOrg = (orgId) => { + // Just use this one? + var baseurl = globalUrl + const url = baseurl + '/api/v1/orgs/'+orgId + fetch(url, { + method: 'GET', + credentials: "include", + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed getting org: ", responseJson.readon) + } else { + setSelectedOrganization(responseJson) + var lists = { + "active": { + "triggers": [], + "features": [], + "sync": [], + }, + "inactive": { + "triggers": [], + "features": [], + "sync": [], + }, + } + + Object.keys(responseJson.sync_features).map(function(key, index) { + console.log(responseJson.sync_features[key]) + }) + + setOrganizationFeatures(lists) + } + }), + ) + .catch(error => { + console.log("Error getting org: ", error) + alert.error("Error getting current organization") + }); + } + const submitUser = (data) => { console.log("INPUT: ", data) @@ -609,6 +670,11 @@ const Admin = (props) => { getUsers() } + if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined) { + //setSelectedOrganization(userdata.active_org) + handleGetOrg(userdata.active_org.id) + } + const paperStyle = { maxWidth: 1250, margin: "auto", @@ -831,19 +897,21 @@ const Admin = (props) => { return ( - - - - - {primaryIcon} - - - - {secondaryIcon} - + + + + + + {primaryIcon} + + + + {secondaryIcon} + + ) } @@ -853,15 +921,21 @@ const Admin = (props) => { { "primary": "Workflows", "secondary": "", - "active": false, + "active": true, "icon": , }, { "primary": "Apps", "secondary": "", - "active": false, + "active": true, "icon": , }, + { + "primary": "Organization", + "secondary": "", + "active": true, + "icon": , + }, ] const cloudSyncModal = @@ -905,7 +979,7 @@ const Admin = (props) => { setCloudSyncApikey(event.target.value) }} /> - +
+ {orgSyncResponse.length > 0 ? + + Error from Shuffle: {orgSyncResponse} + + : null + } + {/* + + {syncList.map((data, index) => { + return ( + + ) + })} + + */} + Cloud sync features + + {Object.keys(selectedOrganization.sync_features).map(function(key, index) { + + // + const item = selectedOrganization.sync_features[key] + const griditem = { + "primary": key, + "secondary": "", + "active": item.active, + "icon": , + } + + return ( + + + + ) + })} + +
+ } +
+ : null + const modalView = { }} > - {curTab === 0 ? "Add user" : "Add environment"} + {curTab === 1 ? "Add user" : "Add environment"} - {curTab === 0 ? + {curTab === 1 ?
Username { onChange={(event) => changeModalData("Password", event.target.value)} />
- : curTab === 2 ? + : curTab === 3 ?
Environment Name { Cancel
- const usersView = curTab === 0 ? + const usersView = curTab === 1 ?

User management

@@ -1145,7 +1420,7 @@ const Admin = (props) => {
: null - const schedulesView = curTab === 3 ? + const schedulesView = curTab === 4 ?

Schedules

@@ -1194,7 +1469,7 @@ const Admin = (props) => {
: null - const appCategoryView = curTab === 6 ? + const appCategoryView = curTab === 7 ?

Categories

@@ -1270,7 +1545,7 @@ const Admin = (props) => {
: null - const authenticationView = curTab === 1 ? + const authenticationView = curTab === 2 ?

App Authentication

@@ -1355,7 +1630,7 @@ const Admin = (props) => {
: null - const environmentView = curTab === 2 ? + const environmentView = curTab === 3 ?

Environments

@@ -1456,7 +1731,7 @@ const Admin = (props) => {
: null - const organizationsTab = curTab === 5 ? + const organizationsTab = curTab === 6 ?

Organizations

@@ -1537,7 +1812,7 @@ const Admin = (props) => {
: null - const hybridTab = curTab === 4 ? + const hybridTab = curTab === 5 ?

Hybrid

@@ -1580,13 +1855,13 @@ const Admin = (props) => { // primary={environment.Registered ? "true" : "false"} const setConfig = (event, newValue) => { - if (newValue === 1) { + if (newValue === 2) { getAppAuthentication() - } else if (newValue === 2) { - getEnvironments() } else if (newValue === 3) { + getEnvironments() + } else if (newValue === 4) { getSchedules() - } else if (newValue === 5) { + } else if (newValue === 6) { getOrgs() } @@ -1608,6 +1883,7 @@ const Admin = (props) => { onChange={setConfig} aria-label="disabled tabs example" > + Organization/> Users /> App Authentication/> Environments/> @@ -1618,6 +1894,7 @@ const Admin = (props) => {
+ {organizationView} {authenticationView} {appCategoryView} {usersView} diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 84ffea4a..3d2f9ebe 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=worker -VERSION=0.6.0 +VERSION=0.8.0 echo "Running docker build with $NAME:$VERSION" CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . From a9cce3e4e8280cf6935974da076e5c9ae6f90f9e Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 7 Nov 2020 10:35:56 +0100 Subject: [PATCH 07/18] Added organizational EDIT possibility --- backend/go-app/main.go | 127 ++++++++++++++++++++++++++- backend/go-app/walkoff.go | 3 +- frontend/src/views/Admin.jsx | 162 ++++++++++++++++++++++++++++------- 3 files changed, 258 insertions(+), 34 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 413d3ec8..4fa7b193 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -72,7 +72,9 @@ var gceProject = "shuffle" var bucketName = "shuffler.appspot.com" var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" -var syncUrl = "http://192.168.3.6:5002" + +//var syncUrl = "http://192.168.102.54:5002" +var syncUrl = "http://localhost:5002" var dbclient *datastore.Client @@ -2825,6 +2827,11 @@ func fixUserOrg(ctx context.Context, user *User) *User { } if userFound { + user.PrivateApps = []WorkflowApp{} + user.Executions = ExecutionInfo{} + user.Limits = UserLimits{} + user.Authentication = []UserAuth{} + org.Users[orgIndex] = *user } else { org.Users = append(org.Users, *user) @@ -7500,6 +7507,123 @@ func handleStopCloudSync(syncUrl string, org Org) error { return nil } +func handleEditOrg(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in cloud setup: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if user.Role != "admin" { + log.Printf("Not admin.") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Not admin"}`)) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`)) + return + } + + type ReturnData struct { + Image string `json:"image" datastore:"image"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + OrgId string `json:"org_id" datastore:"org_id"` + } + + var tmpData ReturnData + err = json.Unmarshal(body, &tmpData) + if err != nil { + log.Printf("Failed unmarshalling test: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + if tmpData.OrgId != user.ActiveOrg.Id || fileId != user.ActiveOrg.Id { + log.Printf("User can't edit the org") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "No permission to edit this org"}`)) + return + } + + ctx := context.Background() + org, err := getOrg(ctx, tmpData.OrgId) + if err != nil { + log.Printf("Organization doesn't exist: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + admin := false + userFound := false + for _, inneruser := range org.Users { + if inneruser.Id == user.Id { + userFound = true + if inneruser.Role == "admin" { + admin = true + } + + break + } + } + + if !userFound { + log.Printf("User %s doesn't exist in organization for edit %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if !admin { + log.Printf("User %s doesn't have edit rights to %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + org.Image = tmpData.Image + org.Name = tmpData.Name + org.Description = tmpData.Description + //log.Printf("Org: %#v", org) + err = setOrg(ctx, *org, org.Id) + if err != nil { + log.Printf("User %s doesn't have edit rights to %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully updated org"}`))) + +} + // INFO: https://docs.google.com/drawings/d/1JJebpPeEVEbmH_qsAC6zf9Noygp7PytvesrkhE19QrY/edit /* This is here to both enable and disable cloud sync features for an organization @@ -8023,6 +8147,7 @@ func initHandlers() { r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") //r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") // Important for email, IDS etc. Create this by: diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 6ce5a77b..5315c75b 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -81,6 +81,7 @@ type SyncFeatures struct { type SyncData struct { Active bool `json:"active" datastore:"active"` + Type string `json:"type" datastore:"type"` Name string `json:"name" datastore:"name"` Description string `json:"description" datastore:"description"` Limit int64 `json:"limit" datastore:"limit"` @@ -95,7 +96,7 @@ type SyncConfig struct { type Org struct { Name string `json:"name" datastore:"name"` Description string `json:"description" datastore:"description"` - Image string `json:"image" datastore:"image"` + Image string `json:"image" datastore:"image,noindex"` Id string `json:"id" datastore:"id"` Org string `json:"org" datastore:"org"` Users []User `json:"users" datastore:"users"` diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 817a20e2..1e65e5b2 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -30,6 +30,7 @@ import { useAlert } from "react-alert"; import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core'; import { useTheme } from '@material-ui/core/styles'; +import HandlePayment from './HandlePayment' import PolymerIcon from '@material-ui/icons/Polymer'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; @@ -253,7 +254,7 @@ const Admin = (props) => { if (disableSync) { alert.success("Successfully disabled sync!") } else { - alert.success("Sync successfully set up!") + alert.success("Cloud Syncronization successfully set up!") } selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync @@ -269,6 +270,51 @@ const Admin = (props) => { }) } + const orgSaveButton = + + + const handleEditOrg = (name, description, orgId, image) => { + const data = { + "name": name, + "description": description, + "org_id": orgId, + "image": image, + } + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed updating org") + } else { + alert.success("Successfully edited org!") + setSelectedUserModalOpen(false) + } + }), + ) + .catch(error => { + alert.error("Err: " + error.toString()) + }); + } + const onPasswordChange = () => { const data = { "username": selectedUser.username, "newpassword": newPassword } const url = globalUrl + '/api/v1/users/passwordchange'; @@ -360,9 +406,13 @@ const Admin = (props) => { } Object.keys(responseJson.sync_features).map(function(key, index) { - console.log(responseJson.sync_features[key]) + //console.log(responseJson.sync_features[key]) }) + + if (responseJson.image !== undefined && responseJson.image !== null && responseJson.image.length > 0) { + setFileBase64(responseJson.image) + } setOrganizationFeatures(lists) } }), @@ -668,6 +718,22 @@ const Admin = (props) => { if (firstRequest) { setFirstRequest(false) getUsers() + + const views = { + "organization": 0, + "users": 1, + "app_auth": 2, + "environments": 3, + "schedules": 4, + "categories": 5, + } + + if (props.match.params.key !== undefined) { + const tmpitem = views[props.match.params.key] + if (tmpitem !== undefined) { + setCurTab(tmpitem) + } + } } if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined) { @@ -1044,25 +1110,33 @@ const Admin = (props) => { const canvasUrl = canvas.toDataURL() if (canvasUrl !== fileBase64) { - //console.log("SET URL TO: ", canvasUrl) setFileBase64(canvasUrl) + selectedOrganization.image = canvasUrl + setSelectedOrganization(selectedOrganization) } } } - const imageData = file.length > 0 ? file : fileBase64 + if (imageData !== undefined && imageData.length === 0) { + //imageData + } + const imageInfo = const organizationView = curTab === 0 ?
- Organization overview - Click here to learn more about organization editing +
+

Organization overview

+ + On this page you can configure individual parts of your organization.Learn more. + +
{selectedOrganization.id === undefined ? null :
-
{upload.click()}}> +
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> upload = ref} onChange={editHeaderImage} /> {imageInfo}
@@ -1111,32 +1185,38 @@ const Admin = (props) => { />
Description - { - selectedOrganization.description = e.target.value - setSelectedOrganization(selectedOrganization) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style:{ - color: "white", - }, - }} - /> +
+ { + selectedOrganization.description = e.target.value + setSelectedOrganization(selectedOrganization) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> +
+ {orgSaveButton} +
+
+ Cloud syncronization What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach.
@@ -1582,9 +1662,9 @@ const Admin = (props) => { primary="Actions" /> - {authentication === undefined ? null : authentication.map(data => { + {authentication === undefined ? null : authentication.map((data, index) => { return ( - + style={{minWidth: 150, maxWidth: 150}} @@ -1869,6 +1949,24 @@ const Admin = (props) => { console.log("Should get apps for categories.") } + const views = { + 0: "organization", + 1: "users", + 2: "app_auth", + 3: "environments", + 4: "schedules", + 5: "categories", + } + + //var theURL = window.location.pathname + //FIXME: Add url edits + //var theURL = window.location + //theURL.replace(`/${views[curTab]}`, `/${views[newValue]}`) + //window.history.pushState({"html":response.html,"pageTitle":response.pageTitle},"", urlPath); + + //console.log(newpath) + //window.location.pathame = newpath + setModalUser({}) setCurTab(newValue) } From 1193cf6aaff9c1dbf27dc5b36481f58a0c4cb20d Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 7 Nov 2020 10:43:01 +0100 Subject: [PATCH 08/18] Added default org image --- backend/go-app/main.go | 2 +- frontend/src/views/Admin.jsx | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 4fa7b193..273d2943 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3586,7 +3586,7 @@ func executeCloudAction(action CloudSyncJob, apikey string) error { } if !responseData.Success { - return errors.New(fmt.Sprintf("Error from Shuffler: %s", responseData.Reason)) + return errors.New(fmt.Sprintf("Cloud error from Shuffler: %s", responseData.Reason)) } return nil diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 1e65e5b2..45a77e03 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -24,8 +24,6 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import IconButton from '@material-ui/core/IconButton'; import Avatar from '@material-ui/core/Avatar'; import Zoom from '@material-ui/core/Zoom'; - - import { useAlert } from "react-alert"; import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core'; @@ -46,6 +44,8 @@ import ScheduleIcon from '@material-ui/icons/Schedule'; import CloudIcon from '@material-ui/icons/Cloud'; import BusinessIcon from '@material-ui/icons/Business'; +const defaultImage = "data:image/png;base64,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" + const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important" @@ -1117,10 +1117,8 @@ const Admin = (props) => { } } - const imageData = file.length > 0 ? file : fileBase64 - if (imageData !== undefined && imageData.length === 0) { - //imageData - } + var imageData = file.length > 0 ? file : fileBase64 + imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData const imageInfo = const organizationView = curTab === 0 ? @@ -1128,7 +1126,7 @@ const Admin = (props) => {

Organization overview

- On this page you can configure individual parts of your organization.Learn more. + On this page you can configure individual parts of your organization. Learn more
{selectedOrganization.id === undefined ? From b72e436d8eefb45eea5e61eb5a5ed065662d5297 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 7 Nov 2020 11:45:46 +0100 Subject: [PATCH 09/18] Started Schedule cloud integration --- backend/go-app/main.go | 3 +- backend/go-app/walkoff.go | 40 +++++++++++++ frontend/src/App.jsx | 2 +- frontend/src/views/AngularWorkflow.jsx | 81 ++++++++++++++++++-------- 4 files changed, 100 insertions(+), 26 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 273d2943..1d2bed7c 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -279,6 +279,7 @@ type ScheduleOld struct { CreationTime int64 `json:"creationtime" datastore:"creationtime,noindex"` LastModificationtime int64 `json:"lastmodificationtime" datastore:"lastmodificationtime,noindex"` LastRuntime int64 `json:"lastruntime" datastore:"lastruntime,noindex"` + Frequency string `json:"frequency" datastore:"frequency,noindex"` } // Returned from /GET /schedules @@ -3695,7 +3696,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { - log.Printf("Failed cloud action START execution", err) + log.Printf("Failed cloud action START execution: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5315c75b..140924d1 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -318,6 +318,7 @@ type Schedule struct { ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` Id string `json:"id" datastore:"id"` OrgId string `json:"org_id" datastore:"org_id"` + Environment string `json:"environment" datastore:"environment"` } type Workflow struct { @@ -3305,6 +3306,45 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { } } + if schedule.Environment == "cloud" { + log.Printf("[INFO] Should START a cloud schedule for workflow %s with schedule ID %s", workflow.ID, schedule.Id) + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + // 1 = scheduleId + // 2 = schedule (cron, frequency) + // 3 = workflowId + // 4 = execution argument + action := CloudSyncJob{ + Type: "schedule", + Action: "start", + OrgId: org.Id, + PrimaryItemId: schedule.Id, + SecondaryItem: schedule.Frequency, + ThirdItem: workflow.ID, + FourthItem: schedule.ExecutionArgument, + } + + log.Printf("Action: %#v", action) + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action START schedule", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully set up cloud action schedule") + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Done"}`))) + return + } + } + log.Printf("Schedulearg: %s", parsedBody) err = createSchedule( diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 16c2ca17..668324d8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -117,7 +117,7 @@ const App = (message, props) => { // Dumb for content load (per now), but good for making the site not suddenly reload parts (ajax thingies) const options = { - timeout: 5000, + timeout: 9000, position: positions.BOTTOM_LEFT, }; diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index eec5bf38..ee973c89 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1582,6 +1582,7 @@ const AngularWorkflow = (props) => { "name": trigger.name, "frequency": workflow.triggers[triggerindex].parameters[0].value, "execution_argument": workflow.triggers[triggerindex].parameters[1].value, + "environment": workflow.triggers[triggerindex].environment, "id": trigger.id, } @@ -1920,9 +1921,9 @@ const AngularWorkflow = (props) => { "description": "Wait for user input", "trigger_type": "USERINPUT", "errors": null, - "is_valid": cloudSyncEnabled, + "is_valid": true, "label": "User input", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "onprem", "long_description": "Take user input to continue execution", }, { @@ -1933,9 +1934,9 @@ const AngularWorkflow = (props) => { "trigger_type": "SCHEDULE", "errors": null, "large_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAAAAABVicqIAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfjCB8QNSt2pVcCAAAIxUlEQVRo3u2aa4xV1RXH/2vtfWeAYQbBBwpUBqGkjQLKw4LQ1NqmDy2RxmKJSGxiaatttbWhabQPSFuT2i+NqdFKbVqjjDZGYxqgxFbbWgHlNSkBChQj8ii+ZV4Mc/be/34459w5995zH4zYpA3709x7z9m/vdbae6/XCPH+D/0vMM5AzkDOQBoY9hSfJ4n4khCISGMvySlcKww0UvYNpAFdNAxhEAXQc/T1g2/2RFJoOW/8OeNHAaDXepwGIYEGOL5146a9/z5R/LJp7KRp86+4UOJf3yskUKVv/ZN/OQoAogIIQQYAaJ117aL2ehjWHcEF7lsxEQK1RgeNLaLGKgSti5919L76DPUhzvPAV0cAanL3khgDyMf+GOjCUCHB8Z0VI6GFGsYVq8Cnt1cXpg7Eez7ZXiKEiKgxqqqSFUcx7M5+uqFAHLuWQwYRYqzNfsjAjWLmdka5Kqu5u5ztvOkfNoTko4oHICNHtzSHqK+rywMwCEyZruX+ZV5zLFcL4uzTy7qtT24RZUDh4ivmTL2wdbgN/mTvkZd3bO58F2KKTwT+aGXIu2uq6yribxQmMYRRYMZPO8uVfmTNouGx4QFALW6OQqX5q0McV8MkR0wNdOEzAyRd5H2Ih3cuMHD3N0ZB0+ea8MUBhoYhER9GcimJUXz0eTJEvvx9H/nAA19pQrwFRApY6iueqgZxXF9ItCsWZz0Y6KocNu8Ct8xBqjKL2+kbg3juH5vao4D5/6SrcgRiDPu/J4nYanF/+XnJhwT2z0v8mRjc0l/jyojldvz9qHhRotL81zJKPsTxjoShBj+uefklq4q4KRXd4NKeUuMjn7E21ZXFPVWOcdkY4PaxScRgcUepKHmQwJ5Liqta1RiDjLi5LaaImL+VUPIgjj+LlSUWt1Saw3vvK7cpGbEjsb7BfJdVWA4k8Nj5UAHEYt5JNiZHTFmZWNLgt1lRciCOK2FFAEHLjvLdGHhi+eev/8J112ytOA0MwX08VrPi0uzBr4QEvj4BEhvwbkYVv3adC4HiDznOw3P7SKgAMOjI/F7p8AI6DlsCUDf9NlTGB9oMaw3yAgd1l92exqSrs8FppSBuNgwAUTxeudrA7gugUKzNc4OBr10YvyzmpUF9VkhCbN4qAYCGuYvz1pveaHkeSPx5X4MAoPonUHRVOZCnYeOfbxWfM1F/BIJVInXFstFOABDrnWEVCI1/BgGA+kmfy5mJ6Pf5q0tEmXAtFACxcweqQrBnFwIAwWdH+0qdCOqF8tcjAKDBCwhVIS9GhgCIhVVmqRnYKha0J4Z+oWi3Sqm3xSsO4y8fSoYkvnV+bHp09qVGKZuHBjuT72eOCQ3mOGVjbiLvkWPIhwA9h0EAgukYYtllNrwA1BP7qkCI195EbJIZQ4MIJoyGAFAcqirJWz0ggICx+ecNI5sACFxVyLjk6sOR9Dsbrx+gAEDQ4xACQnsWSEr65uAwAmH1PSbUs5M/j6bv2XSO9LLoSyLXEaOgjWa3pRqXtuSv3hLIu7CuRwHAjetKfjFvjxgQFlrAUOgbMbxyruqYpmSKgYy6gm5dYk2/gBA2DcADII5/UgBqE2GOT3tqOCuFCrWz6+wqSHr+LqP28tkUk3YP3tqBPRdAIZj5HENuNOa5EAaAxQ3pa4gd7mNGraqKDKYXoiKipoCp+zOeNrB7AgQQmGUH6XN9ypUJZHnqcpCEAB2an3gWcNG+rHsKfOccWADG4Oyf91XGfYH+kgTyw9R5Iw001qjmeCiDSbvLXGC4pxWqcTo6fV2FzjwPnYXYzT9YBmHEx4ya8j1r0b67Ml751xKBUYEayOL99CUYx01ITvz6UnWlGtPSjK+Ai/ZUunIXuGE21ApgDNpW9ZbozPGXsZfH8AMlhk8ppnRTWkzZmxcueMeT950LNRCxig894TM7w/FGGACKD/aloVcmWonYYTIFH7GYvK9KZu48jy63MCqiRnDN7mIkF9jVDgVgcF3x5WxIVLrHCphSjRHXWzYuiFNSNWi+N5XFcV186Vr8ohgZlsRdA+wwYlJdTd7L2ulVeGgcjAGkGb9KH3W8KTGJbCkqsTS4i7hGjYEILCbVZCQ6+3oTjLH41ODWezX1JtOiog7LIsiIHUaNqEX7rjoMMjjP7VdBTWFTuuiId8PGBv3u4PvlYWpsfYuJ9Rmxzvyjk/Ht9NnAYx+AojxMrYiFI3YYg8m7G2GQdI5v/eRQqpiId8IKAItP1EwdIj5e3x5ZnYXidJ7bW9KsY01mhpwkKOKvX2qYQTKkSWUI0VVpEnRZ7SSI9GTdnDpvRFyVuHODh+ukcyy9vxvmRVwTuyOxWFAvMS0XyzeaYm9sjXM5Eft8ydrqQTx39IdGhBngtrGIfYXFd+oXC0qW9xDuyvWypSNE3DhY9pje1UDZI8NYrYovdderSjjHx9riEwLBsL83VMApMh6BqsWcnTWF8Y4nVkBt6iEeaKwUlbycuDGD1ntdmZfNIgI3zoLR1EN8q9GiWsx4NHFiRvGRZ8ngqpQHv9wEW4xIbwwNlwdJz4Nj0kaRGshn1p4k6SJXVujcdWsb1CYBtcWSUyl0koFrmpEWIo0CF6/aVm6Zw49cMywtgYsYi5tdzoavVXwOuuGGtwsuU3y2H55/+ZT21hE2hP7eI/s7X+w8DjFJCVyUIb/4XLOM7s3+pVuSMrqARjwBtI5qbeZAb/fxAMBISDppYtzIB5bmltHrNQR6bwNsMbQULekBZD6INZi1o8p5qt/a2DC1rD8jqqpamiEZg+HfPzG01gYZHLt/0AYxtZo0RoGrO4fcpCHpPF/5ZhugNie9E1FrAblyHd9Du4mxg335rilp4yyrN2MNBK1LnvM1a8cNtwBP/OmpP78aLz4WiHF3pm3u1Ysm1mkBnkozs3vbxk17jmabmRfNmD9vwmlqZsYLhwHQe/SNV97ojrTQcv74MRPaAIRwutqyCYdl853uBnM6LdNqxPvUKh/y+P/594UzkDOQ/3HIfwCAE6puXSx5zQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wOC0zMVQxNjo1Mzo0My0wNDowMGtSg1gAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDgtMzFUMTY6NTM6NDMtMDQ6MDAaDzvkAAAAAElFTkSuQmCC", - "is_valid": true, "label": "Schedule", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "is_valid": true, + "environment": "onprem", "long_description": "Create a schedule based on cron", }, { @@ -5020,27 +5021,59 @@ const AngularWorkflow = (props) => { placeholder={selectedTrigger.label} onChange={selectedTriggerChange} /> + {showEnvironment ? +
+ + Environment + + +
+ : null}
From 3ece0857afbc11369873e811868bcd7c63b1a5ec Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 10 Nov 2020 03:05:58 +0100 Subject: [PATCH 10/18] Fixed typo in app creator --- frontend/src/views/AppCreator.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index c2240024..f47330c9 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1569,7 +1569,7 @@ const AppCreator = (props) => { required style={{flex: "1", marginRight: "15px", marginTop: "5px", backgroundColor: inputColor}} fullWidth={true} - placeholder={"Accept application/json\r\nContent-Type application/json"} + placeholder={"Accept: application/json\r\nContent-Type: application/json"} margin="normal" variant="outlined" id="standard-required" @@ -1577,7 +1577,7 @@ const AppCreator = (props) => { multiline rows="5" onChange={e => setActionField("headers", e.target.value)} - helperText={Headers that are part of the request} + helperText={Headers that are part of the request. Default: EMPTY} InputProps={{ classes: { notchedOutline: classes.notchedOutline, From 83089d2d39f839d1cc7177de508d20696512e2ef Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 13 Nov 2020 14:24:17 +0100 Subject: [PATCH 11/18] Started work on user input trigger --- backend/go-app/main.go | 18 +++++ backend/go-app/walkoff.go | 101 ++++++++++++++++++++++- frontend/src/views/AngularWorkflow.jsx | 106 ++++++++++++++++++------- 3 files changed, 196 insertions(+), 29 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 1d2bed7c..406a7c7b 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -280,6 +280,7 @@ type ScheduleOld struct { LastModificationtime int64 `json:"lastmodificationtime" datastore:"lastmodificationtime,noindex"` LastRuntime int64 `json:"lastruntime" datastore:"lastruntime,noindex"` Frequency string `json:"frequency" datastore:"frequency,noindex"` + Environment string `json:"environment" datastore:"environment"` } // Returned from /GET /schedules @@ -1184,6 +1185,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { } neworg, err := getOrg(ctx, org.Id) + log.Printf("Users: %d", len(org.Users)) if err == nil { log.Printf("Updating org %s with user %s", org.Name, newUser.Username) neworg.Users = append(org.Users, *newUser) @@ -2817,6 +2819,8 @@ func fixUserOrg(ctx context.Context, user *User) *User { continue } + log.Printf("Org users: %d", len(org.Users)) + orgIndex := 0 userFound := false for index, orgUser := range org.Users { @@ -2838,6 +2842,7 @@ func fixUserOrg(ctx context.Context, user *User) *User { org.Users = append(org.Users, *user) } + log.Printf("Users 2: %d", len(org.Users)) err = setOrg(ctx, *org, orgId) if err != nil { log.Printf("Failed setting org %s", orgId) @@ -7190,6 +7195,12 @@ func runInit(ctx context.Context) { continue } + log.Printf("ENV: %s", item.Environment) + if item.Environment == "cloud" { + log.Printf("Skipping cloud schedule") + continue + } + item.Org = activeOrgs[0].Id err = setSchedule(ctx, item) if err != nil { @@ -7241,6 +7252,11 @@ func runInit(ctx context.Context) { } else { log.Printf("Setting up %d schedule(s)", len(schedules)) for _, schedule := range schedules { + if schedule.Environment == "cloud" { + log.Printf("Skipping cloud schedule") + continue + } + //log.Printf("Schedule: %#v", schedule) job := func() { request := &http.Request{ @@ -8146,6 +8162,8 @@ func initHandlers() { // NEW for 0.8.0 r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") + + // Orgs r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 140924d1..1eef8da3 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -85,6 +85,8 @@ type SyncData struct { Name string `json:"name" datastore:"name"` Description string `json:"description" datastore:"description"` Limit int64 `json:"limit" datastore:"limit"` + StartDate int64 `json:"start_date" datastore:"start_date"` + EndDate int64 `json:"end_date" datastore:"end_date"` } type SyncConfig struct { @@ -354,6 +356,7 @@ type Workflow struct { Name string `json:"name" datastore:"name"` Value string `json:"value" datastore:"value,noindex"` } `json:"execution_variables,omitempty" datastore:"execution_variables"` + ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` } type ActionResult struct { @@ -572,6 +575,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode LastModificationtime: timeNow, LastRuntime: timeNow, Org: orgId, + Environment: "onprem", } err = setSchedule(ctx, schedule) @@ -1461,7 +1465,27 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("Has %d actions already", len(newActions)) } + for _, item := range workflow.Actions { + item.ID = uuid.NewV4().String() + newActions = append(newActions, item) + } + + newTriggers := []Trigger{} + for _, item := range workflow.Triggers { + item.Status = "uninitialized" + item.ID = uuid.NewV4().String() + newTriggers = append(newTriggers, item) + } + + newSchedules := []Schedule{} + for _, item := range workflow.Schedules { + item.Id = uuid.NewV4().String() + newSchedules = append(newSchedules, item) + } + workflow.Actions = newActions + workflow.Triggers = newTriggers + workflow.Schedules = newSchedules workflow.IsValid = true workflow.Configuration.ExitOnError = false @@ -3025,10 +3049,61 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { return } + schedule, err := getSchedule(ctx, scheduleId) + if err != nil { + log.Printf("Failed finding schedule %s", scheduleId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Printf("Schedule: %#v", schedule) + + if schedule.Environment == "cloud" { + log.Printf("[INFO] Should STOP a cloud schedule for workflow %s with schedule ID %s", fileId, scheduleId) + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + // 1. Send request to cloud + // 2. Remove schedule if success + action := CloudSyncJob{ + Type: "schedule", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: scheduleId, + SecondaryItem: schedule.Frequency, + ThirdItem: workflow.ID, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action STOP schedule", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully ran cloud action STOP schedule") + err = DeleteKey(ctx, "schedules", scheduleId) + if err != nil { + log.Printf("Failed deleting cloud schedule onprem..") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting cloud schedule"}`))) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) + return + } + } + err = deleteSchedule(ctx, scheduleId) if err != nil { log.Printf("Failed deleting schedule: %s", err) - if strings.Contains(err.Error(), "Job not found") { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) @@ -3329,8 +3404,30 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { FourthItem: schedule.ExecutionArgument, } - log.Printf("Action: %#v", action) + timeNow := int64(time.Now().Unix()) + newSchedule := ScheduleOld{ + Id: schedule.Id, + WorkflowId: workflow.ID, + StartNode: startNode, + Argument: string(schedule.ExecutionArgument), + WrappedArgument: parsedBody, + CreationTime: timeNow, + LastModificationtime: timeNow, + LastRuntime: timeNow, + Org: org.Id, + Frequency: schedule.Frequency, + Environment: "cloud", + } + err = setSchedule(ctx, newSchedule) + if err != nil { + log.Printf("Failed setting cloud schedule: returning", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + log.Printf("Action: %#v", action) err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { log.Printf("Failed cloud action START schedule", err) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index ee973c89..b8e15488 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -796,6 +796,7 @@ const AngularWorkflow = (props) => { "Testing", "Http", ] + const getAppAuthentication = () => { fetch(globalUrl+"/api/v1/apps/authentication", { method: 'GET', @@ -4860,6 +4861,9 @@ const AngularWorkflow = (props) => { // email,sms,app ... workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "type", "value": "email"} + + workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "email", "value": "test@test.com"} + workflow.triggers[selectedTriggerIndex].parameters[4] = {"name": "sms", "value": "0000000"} setWorkflow(workflow) } @@ -4868,7 +4872,7 @@ const AngularWorkflow = (props) => {

{selectedTrigger.app_name}: {selectedTrigger.status}

- What are user inputs? + What is the user input trigger?
@@ -4949,33 +4953,79 @@ const AngularWorkflow = (props) => {
- { - setTriggerOptionsWrapper("email") - }} - color="primary" - value="email" - /> - } - label={
Email
} - /> - { - setTriggerOptionsWrapper("sms") - }} - color="primary" - value="sms" - /> - } - label={
SMS
} - /> + { + setTriggerOptionsWrapper("email") + }} + color="primary" + value="email" + /> + } + label={
Email
} + /> + { + setTriggerOptionsWrapper("sms") + }} + color="primary" + value="sms" + /> + } + label={
SMS
} + />
+ {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ? + { + workflow.triggers[selectedTriggerIndex].parameters[3].value = event.target.value + setWorkflow(workflow) + setUpdate(Math.random()) + }} + /> + : null + } + {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") ? + { + workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value + setWorkflow(workflow) + setUpdate(Math.random()) + }} + /> + : null + }
) @@ -5996,7 +6046,9 @@ const AngularWorkflow = (props) => { setNewAppAuth(newAuthOption) appAuthentication.push(newAuthOption) setAppAuthentication(appAuthentication) + getAppAuthentication() setUpdate(authenticationOption.id) + } return ( From 8abd1584aa45239cce73455bf9ed5644fada0dc3 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 14 Nov 2020 08:29:16 +0100 Subject: [PATCH 12/18] Fixed SMS and Email cloud services --- backend/go-app/main.go | 5 +- backend/go-app/walkoff.go | 102 +++++++++++++++++++++++++ frontend/src/views/AngularWorkflow.jsx | 8 +- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 406a7c7b..97613010 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6644,6 +6644,7 @@ type CloudSyncJob struct { SecondaryItem string `json:"secondary_item" datastore:"secondary_item"` ThirdItem string `json:"third_item" datastore:"third_item"` FourthItem string `json:"fourth_item" datastore:"fourth_item"` + FifthItem string `json:"fifth_item" datastore:"fifth_item"` Created string `json:"created" datastore:"created"` } @@ -7231,7 +7232,7 @@ func runInit(ctx context.Context) { job := func() { err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("Failed request with remote org setup (2): %s", err) + log.Printf("[ERROR] Failed request with remote org setup (2): %s", err) } } @@ -7844,7 +7845,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { job := func() { err := remoteOrgJobHandler(*org, interval) if err != nil { - log.Printf("Failed request with remote org setup (1): %s", err) + log.Printf("[ERROR] Failed request with remote org setup (1): %s", err) } } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 1eef8da3..baa32501 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1817,6 +1817,108 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } else if hook.Id == "" { trigger.Status = "stopped" } + } else if trigger.TriggerType == "USERINPUT" { + // E.g. check email + sms := "" + email := "" + triggerType := "" + triggerInformation := "" + for _, item := range trigger.Parameters { + if item.Name == "alertinfo" { + triggerInformation = item.Value + } else if item.Name == "type" { + triggerType = item.Value + } else if item.Name == "email" { + email = item.Value + } else if item.Name == "sms" { + sms = item.Value + } + } + + // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms + if strings.Contains(triggerType, "email") { + if email == "test@test.com" { + log.Printf("Email isn't specified during save.") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Email field in user input can't be empty"}`))) + return + } + + // triggerid, start node, workflowid, argument + /* + startNode := "" + referenceExecutionId := "" + action := CloudSyncJob{ + Type: "user_input", + Action: "send_email", + OrgId: user.ActiveOrg.Id, + PrimaryItemId: workflow.ID, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: email, + FifthItem: referenceExecutionId, + } + + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed email send to cloud", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed email send to cloud", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + */ + + log.Printf("Should send email to %s during execution.", email) + } + if strings.Contains(triggerType, "sms") { + if sms == "0000000" { + log.Printf("Email isn't specified during save.") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SMS field in user input can't be empty"}`))) + return + } + + /* + startNode := "" + referenceExecutionId := "" + action := CloudSyncJob{ + Type: "user_input", + Action: "send_sms", + OrgId: user.ActiveOrg.Id, + PrimaryItemId: workflow.ID, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: sms, + FifthItem: referenceExecutionId, + } + + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed email send to cloud", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed email send to cloud", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + */ + + log.Printf("Should send SMS to %s during execution.", sms) + } } //log.Println("TRIGGERS") diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index b8e15488..7aff28dc 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1924,7 +1924,7 @@ const AngularWorkflow = (props) => { "errors": null, "is_valid": true, "label": "User input", - "environment": "onprem", + "environment": "cloud", "long_description": "Take user input to continue execution", }, { @@ -4995,7 +4995,8 @@ const AngularWorkflow = (props) => { fullWidth color="primary" placeholder={"mail1@company.com,mail2@company.com"} - onChange={(event) => { + defaultValue={workflow.triggers[selectedTriggerIndex].parameters[3].value} + onBlur={(event) => { workflow.triggers[selectedTriggerIndex].parameters[3].value = event.target.value setWorkflow(workflow) setUpdate(Math.random()) @@ -5018,7 +5019,8 @@ const AngularWorkflow = (props) => { fullWidth color="primary" placeholder={"+474823212,+460203042"} - onChange={(event) => { + defaultValue={workflow.triggers[selectedTriggerIndex].parameters[4].value} + onBlur={(event) => { workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value setWorkflow(workflow) setUpdate(Math.random()) From 3f98f986e79983f74ef4cad13b3b50501e12b45a Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 15 Nov 2020 16:28:20 +0100 Subject: [PATCH 13/18] Fixed hybrid issues --- backend/go-app/main.go | 3 +- backend/go-app/walkoff.go | 23 +- frontend/src/App.jsx | 2 +- frontend/src/components/history.jsx | 8 + frontend/src/views/Admin.jsx | 390 ++++++++++++++++++------- frontend/src/views/AngularWorkflow.jsx | 19 +- frontend/src/views/Apps.jsx | 67 +++-- frontend/src/views/HandlePayment.js | 9 + 8 files changed, 369 insertions(+), 152 deletions(-) create mode 100644 frontend/src/components/history.jsx create mode 100644 frontend/src/views/HandlePayment.js diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 97613010..f3b3f789 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -74,7 +74,8 @@ var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" //var syncUrl = "http://192.168.102.54:5002" -var syncUrl = "http://localhost:5002" +//var syncUrl = "http://localhost:5002" +var syncUrl = "https://shuffler.io" var dbclient *datastore.Client diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index baa32501..0b95ed20 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -68,15 +68,19 @@ type ExecutionRequest struct { } type SyncFeatures struct { - Webhook SyncData `json:"webhook" datastore:"webhook"` - Schedule SyncData `json:"schedule" datastore:"schedule"` - UserInput SyncData `json:"user_input" datastore:"user_input"` - EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` - Apps SyncData `json:"apps" datastore:"apps"` - Workflows SyncData `json:"workflows" datastore:"workflows"` - Schedules SyncData `json:"schedules" datastore:"schedules"` - Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` - Authentication SyncData `json:"authentication" datastore:"authentication"` + Webhook SyncData `json:"webhook" datastore:"webhook"` + Schedules SyncData `json:"schedules" datastore:"schedules"` + UserInput SyncData `json:"user_input" datastore:"user_input"` + SendMail SyncData `json:"send_mail" datastore:"send_mail"` + SendSms SyncData `json:"send_sms" datastore:"send_sms"` + EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` + AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` + WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` + Apps SyncData `json:"apps" datastore:"apps"` + Workflows SyncData `json:"workflows" datastore:"workflows"` + Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` + Authentication SyncData `json:"authentication" datastore:"authentication"` + Schedule SyncData `json:"schedule" datastore:"schedule"` } type SyncData struct { @@ -1836,6 +1840,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms + _ = triggerInformation if strings.Contains(triggerType, "email") { if email == "test@test.com" { log.Printf("Email isn't specified during save.") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 668324d8..5bdda5b9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -100,7 +100,7 @@ const App = (message, props) => { //console.log(responseJson.success) setUserData(responseJson) setIsLoggedIn(true) - console.log("Cookies: ", cookies) + //console.log("Cookies: ", cookies) // Updating cookie every request for (var key in responseJson["cookies"]) { diff --git a/frontend/src/components/history.jsx b/frontend/src/components/history.jsx new file mode 100644 index 00000000..71ce4bd5 --- /dev/null +++ b/frontend/src/components/history.jsx @@ -0,0 +1,8 @@ +import { createBrowserHistory } from 'history'; + +var localExport +if (typeof window !== 'undefined') { + localExport = createBrowserHistory({forceRefresh: true}); +} + +export default localExport diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 45a77e03..25b9ead8 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -75,6 +75,9 @@ const Admin = (props) => { const [users, setUsers] = React.useState([]); const [organizations, setOrganizations] = React.useState([]); const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); + const [userSettings, setUserSettings] = React.useState({}); + const [orgName, setOrgName] = React.useState(""); + const [orgDescription, setOrgDescription] = React.useState(""); const [environments, setEnvironments] = React.useState([]); const [authentication, setAuthentication] = React.useState([]); @@ -86,6 +89,7 @@ const Admin = (props) => { const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false) const [showArchived, setShowArchived] = React.useState(false) + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const getApps = () => { fetch(globalUrl+"/api/v1/workflows/apps", { method: 'GET', @@ -275,7 +279,7 @@ const Admin = (props) => { style={{ width: 150, height: 55, flex: 1 }} variant="contained" color="primary" - onClick={() => handleEditOrg(selectedOrganization.name, selectedOrganization.description, selectedOrganization.id, selectedOrganization.image)} + onClick={() => handleEditOrg(orgName, orgDescription, selectedOrganization.id, selectedOrganization.image)} > Save Changes @@ -303,7 +307,7 @@ const Admin = (props) => { .then(response => response.json().then(responseJson => { if (responseJson["success"] === false) { - alert.error("Failed updating org") + alert.error("Failed updating org: ", responseJson.reason) } else { alert.success("Successfully edited org!") setSelectedUserModalOpen(false) @@ -386,37 +390,44 @@ const Admin = (props) => { 'Content-Type': 'application/json', }, }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - alert.error("Failed getting org: ", responseJson.readon) - } else { - setSelectedOrganization(responseJson) - var lists = { - "active": { - "triggers": [], - "features": [], - "sync": [], - }, - "inactive": { - "triggers": [], - "features": [], - "sync": [], - }, - } + .then(response => { + if (response.status === 401) { + } - Object.keys(responseJson.sync_features).map(function(key, index) { - //console.log(responseJson.sync_features[key]) - }) - - - if (responseJson.image !== undefined && responseJson.image !== null && responseJson.image.length > 0) { - setFileBase64(responseJson.image) - } - setOrganizationFeatures(lists) + return response.json() + }) + .then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed getting org: ", responseJson.readon) + } else { + setSelectedOrganization(responseJson) + var lists = { + "active": { + "triggers": [], + "features": [], + "sync": [], + }, + "inactive": { + "triggers": [], + "features": [], + "sync": [], + }, } - }), - ) + + + // FIXME: Set up features + Object.keys(responseJson.sync_features).map(function(key, index) { + //console.log(responseJson.sync_features[key]) + }) + + setOrgName(responseJson.name) + setOrgDescription(responseJson.description) + if (responseJson.image !== undefined && responseJson.image !== null && responseJson.image.length > 0) { + setFileBase64(responseJson.image) + } + setOrganizationFeatures(lists) + } + }) .catch(error => { console.log("Error getting org: ", error) alert.error("Error getting current organization") @@ -629,7 +640,7 @@ const Admin = (props) => { }) .then((responseJson) => { if (responseJson.success) { - console.log(responseJson.data) + //console.log(responseJson.data) setAuthentication(responseJson.data) } else { alert.error("Failed getting authentications") @@ -715,9 +726,38 @@ const Admin = (props) => { }); } + const getSettings = () => { + fetch(globalUrl+"/api/v1/getsettings", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting settings :O!") + } + + return response.json() + }) + .then((responseJson) => { + setUserSettings(responseJson) + }) + .catch(error => { + console.log(error) + }); + } + + if (firstRequest) { setFirstRequest(false) - getUsers() + if (!isCloud) { + getUsers() + } else { + getSettings() + } const views = { "organization": 0, @@ -737,7 +777,7 @@ const Admin = (props) => { } if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined) { - //setSelectedOrganization(userdata.active_org) + setSelectedOrganization(userdata.active_org) handleGetOrg(userdata.active_org.id) } @@ -1120,8 +1160,51 @@ const Admin = (props) => { var imageData = file.length > 0 ? file : fileBase64 imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData + const cancelSubscriptions = (subscription_id) => { + console.log(selectedOrganization) + const data = { + "subscription_id": subscription_id, + "action": "cancel", + "org_id": selectedOrganization.id, + } + + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(function(response) { + if (response.status !== 200) { + console.log("Error in response") + } + + handleGetOrg(selectedOrganization.id) + return response.json(); + }).then(function(responseJson) { + if (responseJson.success !== undefined && responseJson.success) { + alert.success("Successfully stopped subscription!") + + + } else { + alert.error("Failed stopping subscription. Please contact us.") + } + }) + .catch(function(error) { + console.log("Error: ", error) + alert.error("Failed stopping subscription. Please contact us.") + }) + } + const imageInfo = - const organizationView = curTab === 0 ? + const organizationView = curTab === 0 && selectedOrganization.id !== undefined ?

Organization overview

@@ -1130,17 +1213,18 @@ const Admin = (props) => {
{selectedOrganization.id === undefined ? - null : +
+ :
- +
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> upload = ref} onChange={editHeaderImage} /> {imageInfo}
-
+
Name { id="standard-required" margin="normal" variant="outlined" - defaultValue={selectedOrganization.name} + value={orgName} onChange={e => { const invalid = ["#", ":", "."] for (var key in invalid) { @@ -1166,8 +1250,7 @@ const Admin = (props) => { return } - selectedOrganization.name = e.target.value - setSelectedOrganization(selectedOrganization) + setOrgName(e.target.value) }} color="primary" InputProps={{ @@ -1193,10 +1276,9 @@ const Admin = (props) => { margin="normal" variant="outlined" placeholder="A description for the service" - defaultValue={selectedOrganization.description} + value={orgDescription} onChange={e => { - selectedOrganization.description = e.target.value - setSelectedOrganization(selectedOrganization) + setOrgDescription(e.target.value) }} InputProps={{ classes: { @@ -1214,65 +1296,110 @@ const Admin = (props) => {
- - Cloud syncronization - What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. -
- { - setCloudSyncApikey(event.target.value) - }} - /> - + + Your Apikey + + +
- {orgSyncResponse.length > 0 ? - - Error from Shuffle: {orgSyncResponse} - - : null - } - {/* - - {syncList.map((data, index) => { - return ( - - ) - })} - - */} - Cloud sync features + : +
+
+ { + setCloudSyncApikey(event.target.value) + }} + /> + +
+ {orgSyncResponse.length > 0 ? + + Error from Shuffle: {orgSyncResponse} + + : null + } + {/* + + {syncList.map((data, index) => { + return ( + + ) + })} + + */} +
+ } + Cloud sync features {Object.keys(selectedOrganization.sync_features).map(function(key, index) { + if (key === "schedule") { + return null + } // const item = selectedOrganization.sync_features[key] @@ -1290,8 +1417,51 @@ const Admin = (props) => { ) })} + + {isCloud && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? +
+ + Your subscription{selectedOrganization.subscriptions.length > 1 ? "s" : ""} + + + {selectedOrganization.subscriptions.reverse().map((sub, index) => { + return ( + + + Type: {sub.level}
+ Recurrence: {sub.recurrence}
+ {sub.active ? +
+ Started: {new Date(sub.startdate*1000).toISOString()}
+ +
+ : +
+ Cancelled: {new Date(sub.cancellationdate*1000).toISOString()}
+ + Status: Deactivated + +
+ } + + + ) + })} + + +
+ : null + }
} + +
+ +
: null @@ -1508,7 +1678,11 @@ const Admin = (props) => { + { style={{maxWidth: 200}} primary={schedule.seconds} /> + { // primary={environment.Registered ? "true" : "false"} const setConfig = (event, newValue) => { - if (newValue === 2) { + if (newValue === 1) { + getUsers() + } else if (newValue === 2) { getAppAuthentication() } else if (newValue === 3) { getEnvironments() @@ -1980,10 +2160,10 @@ const Admin = (props) => { aria-label="disabled tabs example" > Organization/> - Users /> - App Authentication/> - Environments/> - Schedules /> + {isCloud ? null : Users />} + {isCloud ? null : App Authentication/>} + {isCloud ? null : Environments/>} + {isCloud ? null : Schedules />} {window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null} {window.location.protocol == "http:" && window.location.port === "3000" ? Organizations/> : null} {window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 7aff28dc..2f10d25e 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -191,6 +191,7 @@ const AngularWorkflow = (props) => { const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' useBeforeunload(() => { @@ -997,12 +998,15 @@ const AngularWorkflow = (props) => { setSelectedApp(curapp) } - var env = environments.find(a => a.Name === curaction.environment) - if (!env || env === undefined) { - env = environments[defaultEnvironmentIndex] + if (environments !== undefined) { + var env = environments.find(a => a.Name === curaction.environment) + if (!env || env === undefined) { + env = environments[defaultEnvironmentIndex] + } + + setSelectedActionEnvironment(env) } - setSelectedActionEnvironment(env) setSelectedActionName(curaction.name) setSelectedAction(curaction) @@ -1265,6 +1269,11 @@ const AngularWorkflow = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!") + if (isCloud) { + setEnvironments({"name": "Cloud", "type": "cloud"}) + } else { + setEnvironments({"name": "Onprem", "type": "onprem"}) + } return } @@ -1949,7 +1958,7 @@ const AngularWorkflow = (props) => { "errors": null, "is_valid": true ? false : cloudSyncEnabled, "label": "Email", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "cloud", "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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', "long_description": "Execute a workflow when you get an email", }] diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 7d159d04..9bb8dda4 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -137,6 +137,7 @@ const Apps = (props) => { const [isDropzone, setIsDropzone] = React.useState(false); const upload = React.useRef(null); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" ? true : false const { start, stop } = useInterval({ duration: 5000, @@ -592,7 +593,7 @@ const Apps = (props) => {
{activateButton} - {(props.userdata.role === "admin" || props.userdata.id === selectedApp.owner) || !selectedApp.generated ? + {(props.userdata !== undefined && (props.userdata.role === "admin" || props.userdata.id === selectedApp.owner) || !selectedApp.generated) ?
{downloadButton} {editButton} @@ -618,7 +619,7 @@ const Apps = (props) => { })}
: null} - {props.userdata.id === selectedApp.owner ? + {props.userdata !== undefined && props.userdata.id === selectedApp.owner ?
{/*

ID: {selectedApp.id}

*/} Sharing: @@ -794,7 +795,7 @@ const Apps = (props) => { } const uploadFile = (e) => { - const isDropzone = e.dataTransfer?.files.length > 0; + const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; const files = isDropzone ? e.dataTransfer.files : e.target.files; const reader = new FileReader(); @@ -846,37 +847,41 @@ const Apps = (props) => {
-
+

Your apps ({apps.length+searchableApps.length})

- - - - - - + {isCloud ? null : + + + + + + + + + }
{ + return ( + null + ) +} + +export default HandlePayment From ae39da8ffdb55142c822a415f5fd7148545655e1 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 16 Nov 2020 18:37:04 +0100 Subject: [PATCH 14/18] Fixed most of user input continuations --- backend/go-app/main.go | 131 +++++++++++--- backend/go-app/walkoff.go | 238 +++++++++++++++++-------- frontend/src/views/AngularWorkflow.jsx | 10 +- functions/onprem/orborus/orborus.go | 16 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 122 ++++++++++++- 6 files changed, 400 insertions(+), 119 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index f3b3f789..687016e6 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -74,9 +74,10 @@ var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" //var syncUrl = "http://192.168.102.54:5002" -//var syncUrl = "http://localhost:5002" var syncUrl = "https://shuffler.io" +//var syncUrl = "http://localhost:5002" + var dbclient *datastore.Client type Userapi struct { @@ -3682,9 +3683,10 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { // Let remote endpoint handle access checks (shuffler.io) currentUrl := fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId) + startNode := requestdata.Start if requestdata.Environment == "cloud" { - log.Printf("[INFO] Should START a cloud webhook for url %s", currentUrl) // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + log.Printf("[INFO] Should START a cloud webhook for url %s for startnode %s", currentUrl, startNode) org, err := getOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) @@ -3696,7 +3698,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Action: "start", OrgId: org.Id, PrimaryItemId: newId, - SecondaryItem: requestdata.Start, + SecondaryItem: startNode, ThirdItem: requestdata.Workflow, } @@ -3713,7 +3715,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { hook := Hook{ Id: newId, - Start: requestdata.Start, + Start: startNode, Workflows: []string{requestdata.Workflow}, Info: Info{ Name: requestdata.Name, @@ -4709,7 +4711,7 @@ Please contact us at shuffler.io or frikky@shuffler.io if there is an issue with } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } func setBadMemcache(ctx context.Context, path string) { @@ -5186,7 +5188,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } type OauthToken struct { @@ -6657,22 +6659,34 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio if err != nil { return err } + // FIXME: Handle auth _ = workflow - type execStruct struct { - ExecutionSource string `json:"execution_source"` - ExecutionArgument string `json:"execution_argument"` - Start string `json:"start,omitempty"` - } - - parsedArgument := strings.Replace(string(executionArgument), "\"", "\\\"", -1) - newExec := execStruct{ + parsedArgument := executionArgument + newExec := ExecutionRequest{ ExecutionSource: executionSource, ExecutionArgument: parsedArgument, } - //bodyWrapper := fmt.Sprintf(`{"execution_source": "%s", "execution_argument": "%s"}`, executionSource, parsedArgument) + var execution ExecutionRequest + err = json.Unmarshal([]byte(parsedArgument), &execution) + if err == nil { + log.Printf("FOUND EXEC %#v", execution) + if len(execution.ExecutionArgument) > 0 { + parsedArgument := strings.Replace(string(execution.ExecutionArgument), "\\\"", "\"", -1) + log.Printf("New exec argument: %s", execution.ExecutionArgument) + + if strings.HasPrefix(parsedArgument, "{") && strings.HasSuffix(parsedArgument, "}") { + log.Printf("\nData is most likely JSON from %s\n", newExec.ExecutionSource) + } + + newExec.ExecutionArgument = parsedArgument + } + } else { + log.Printf("Unmarshal issue: %s", err) + } + if len(startNode) > 0 { newExec.Start = startNode } @@ -6694,10 +6708,12 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio } func handleCloudJob(job CloudSyncJob) error { + // May need authentication in all of these..? + log.Printf("Handle job with type %s and action %s", job.Type, job.Action) if job.Type == "webhook" { if job.Action == "execute" { - log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem) + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) if err != nil { log.Printf("Failed executing workflow from cloud hook: %s", err) @@ -6728,14 +6744,79 @@ func handleCloudJob(job CloudSyncJob) error { } } else if job.Type == "user_input" { - if job.Action == "execute" { - log.Printf("Should handle user_input for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) - err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "user_input", job.ThirdItem) + if job.Action == "continue" { + log.Printf("Should handle user_input CONTINUE for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + // FIXME: Handle authorization + ctx := context.Background() + workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) if err != nil { - log.Printf("Failed executing workflow from cloud user_input: %s", err) + return err + } + + if job.PrimaryItemId != workflowExecution.Workflow.ID { + return errors.New("Bad workflow ID when stopping execution.") + } + + workflowExecution.Status = "EXECUTING" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + return err + } + + fullUrl := fmt.Sprintf("https://shuffler.io/api/v1/workflows/%s/execute?authorization=%s&start=%s&reference_execution=%s&answer=true", job.PrimaryItemId, job.FourthItem, job.SecondaryItem, job.ThirdItem) + newRequest, err := http.NewRequest( + "GET", + fullUrl, + nil, + ) + if err != nil { + log.Printf("Failed continuing workflow in request builder: %s", err) + return err + } + + _, _, err = handleExecution(job.PrimaryItemId, Workflow{}, newRequest) + if err != nil { + log.Printf("Failed continuing workflow from cloud user_input: %s", err) + return err } else { log.Printf("Successfully executed workflow from cloud user_input") } + } else if job.Action == "stop" { + log.Printf("Should handle user_input STOP for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + ctx := context.Background() + workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) + if err != nil { + return err + } + + if job.PrimaryItemId != workflowExecution.Workflow.ID { + return errors.New("Bad workflow ID when stopping execution.") + } + + /* + if job.FourthItem != workflowExecution.Authorization { + return errors.New("Bad authorization when stopping execution.") + } + */ + + newResults := []ActionResult{} + for _, result := range workflowExecution.Results { + if result.Action.AppName == "User Input" && result.Result == "Waiting for user feedback based on configuration" { + result.Status = "ABORTED" + result.Result = "Aborted manually by user." + } + + newResults = append(newResults, result) + } + + workflowExecution.Results = newResults + workflowExecution.Status = "ABORTED" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + return err + } + + log.Printf("Successfully updated user input to aborted.") } } else { log.Printf("No handler for type %s and action %s", job.Type, job.Action) @@ -6763,7 +6844,7 @@ func remoteOrgJobController(org Org, body []byte) error { log.Printf("Should stop org job controller") if strings.Contains(responseData.Reason, "Bad apikey") { - log.Printf("Bad apikey. Stopping sync for org!") + log.Printf("Bad apikey. Stopping sync for org?: %s", responseData.Reason) if value, exists := scheduledOrgs[org.Id]; exists { // Looks like this does the trick? Hurr @@ -6786,7 +6867,7 @@ func remoteOrgJobController(org Org, body []byte) error { log.Printf("Successfully updated the org to not sync") } - return errors.New("Stopped schedule for org because of bad apikey.") + return errors.New("Stopped schedule for org locally because of bad apikey.") } else { return errors.New(fmt.Sprintf("Failed finding the schedule for org %s", org.Id)) } @@ -7223,7 +7304,7 @@ func runInit(ctx context.Context) { } //interval := int(org.SyncConfig.Interval) - interval := 5 + interval := 15 if interval == 0 { log.Printf("Skipping org %s because sync isn't set (0).", org.Id) continue @@ -7731,7 +7812,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // return //} - log.Printf("Apidata: %s", tmpData.Apikey) + //log.Printf("Apidata: %s", tmpData.Apikey) // FIXME: Path client := &http.Client{} @@ -7808,7 +7889,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Respbody: %s", string(respBody)) + //log.Printf("Respbody: %s", string(respBody)) responseData := retStruct{} err = json.Unmarshal(respBody, &responseData) if err != nil { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 0b95ed20..729fbdd8 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -56,15 +56,15 @@ var scheduledOrgs = map[string]*newscheduler.Job{} //} type ExecutionRequest struct { - ExecutionId string `json:"execution_id"` - ExecutionArgument string `json:"execution_argument"` - ExecutionSource string `json:"execution_source"` - WorkflowId string `json:"workflow_id"` - Environments []string `json:"environments"` - Authorization string `json:"authorization"` - Status string `json:"status"` - Start string `json:"start"` - Type string `json:"type"` + ExecutionId string `json:"execution_id,omitempty"` + ExecutionArgument string `json:"execution_argument,omitempty"` + ExecutionSource string `json:"execution_source,omitempty"` + WorkflowId string `json:"workflow_id,omitempty"` + Environments []string `json:"environments,omitempty"` + Authorization string `json:"authorization,omitempty"` + Status string `json:"status,omitempty"` + Start string `json:"start,omitempty"` + Type string `json:"type,omitempty"` } type SyncFeatures struct { @@ -685,7 +685,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque //} resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } // FIXME: Authenticate this one? Can org ID be auth enough? @@ -899,6 +899,48 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } } + if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" { + log.Printf("SHOULD WAIT A BIT AND RUN CLOUD STUFF WITH USER INPUT! WAITING!") + + var trigger Trigger + err = json.Unmarshal([]byte(actionResult.Result), &trigger) + if err != nil { + log.Printf("Failed unmarshaling actionresult for user input: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + orgId := workflowExecution.ExecutionOrg + if len(workflowExecution.OrgId) == 0 && len(workflowExecution.Workflow.OrgId) > 0 { + orgId = workflowExecution.Workflow.OrgId + } + + err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId) + if err != nil { + log.Printf("Failed userinput handler: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err))) + } else { + log.Printf("Successful userinput handler") + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "CLOUD IS DONE"}`))) + + actionResult.Result = "Waiting for user feedback based on configuration" + + workflowExecution.Results = append(workflowExecution.Results, actionResult) + workflowExecution.Status = actionResult.Status + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + log.Printf("Failed ") + } else { + log.Printf("Successfully set the execution to waiting.") + } + } + + return + } + if actionResult.Status == "ABORTED" || actionResult.Status == "FAILURE" { log.Printf("Actionresult is %s. Should set workflowExecution and exit all running functions", actionResult.Status) @@ -977,7 +1019,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { result.Status = actionResult.Status - result.Result = "Aborted because of an unknown error" + result.Result = "Aborted because of error in another node" } if len(result.Result) > 0 { @@ -1201,10 +1243,10 @@ func handleExecutionStatistics(execution WorkflowExecution) { for _, result := range execution.Results { resultCheck := JSONCheck(result.Result) if !resultCheck { - log.Printf("Result is NOT JSON!") + //log.Printf("Result is NOT JSON!") continue } else { - log.Printf("Result IS JSON!") + //log.Printf("Result IS JSON!") } @@ -1839,6 +1881,13 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } + if len(triggerType) == 0 { + log.Printf("No type specified for user input node") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No contact option specified in user input"}`))) + return + } + // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms _ = triggerInformation if strings.Contains(triggerType, "email") { @@ -1849,38 +1898,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - // triggerid, start node, workflowid, argument - /* - startNode := "" - referenceExecutionId := "" - action := CloudSyncJob{ - Type: "user_input", - Action: "send_email", - OrgId: user.ActiveOrg.Id, - PrimaryItemId: workflow.ID, - SecondaryItem: startNode, - ThirdItem: triggerInformation, - FourthItem: email, - FifthItem: referenceExecutionId, - } - - org, err := getOrg(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("Failed email send to cloud", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - err = executeCloudAction(action, org.SyncConfig.Apikey) - if err != nil { - log.Printf("Failed email send to cloud", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - */ - log.Printf("Should send email to %s during execution.", email) } if strings.Contains(triggerType, "sms") { @@ -1891,37 +1908,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - /* - startNode := "" - referenceExecutionId := "" - action := CloudSyncJob{ - Type: "user_input", - Action: "send_sms", - OrgId: user.ActiveOrg.Id, - PrimaryItemId: workflow.ID, - SecondaryItem: startNode, - ThirdItem: triggerInformation, - FourthItem: sms, - FifthItem: referenceExecutionId, - } - - org, err := getOrg(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("Failed email send to cloud", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - err = executeCloudAction(action, org.SyncConfig.Apikey) - if err != nil { - log.Printf("Failed email send to cloud", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - */ - log.Printf("Should send SMS to %s during execution.", sms) } } @@ -2346,7 +2332,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { return } } else { - log.Printf("API key %s is correct to abort %s", parsedKey, executionId) + log.Printf("API key to abort/finish execution %s is correct.", executionId) } if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" || workflowExecution.Status == "FINISHED" { @@ -2367,7 +2353,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { result.Status = "ABORTED" - result.Result = "Aborted because of an unknown error" + result.Result = "Aborted because of error in another node" } if len(result.Result) > 0 { @@ -2446,7 +2432,7 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } func handleExecution(id string, workflow Workflow, request *http.Request) (WorkflowExecution, string, error) { @@ -2507,6 +2493,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // This one doesn't really matter. + log.Printf("Running POST execution with data %s", body) var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { @@ -2797,6 +2784,16 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } + for _, trigger := range workflowExecution.Workflow.Triggers { + log.Printf("ID: %s vs %s", trigger.ID, workflowExecution.Start) + if trigger.ID == workflowExecution.Start { + if trigger.AppName == "User Input" { + startFound = true + break + } + } + } + if !startFound { log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) @@ -3509,6 +3506,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { SecondaryItem: schedule.Frequency, ThirdItem: workflow.ID, FourthItem: schedule.ExecutionArgument, + FifthItem: startNode, } timeNow := int64(time.Now().Unix()) @@ -6128,3 +6126,85 @@ func removeOutlookTriggerFunction(ctx context.Context, triggerId string) error { _ = resp return nil } + +func handleUserInput(trigger Trigger, organizationId string, workflowId string, referenceExecution string) error { + // E.g. check email + sms := "" + email := "" + triggerType := "" + triggerInformation := "" + for _, item := range trigger.Parameters { + if item.Name == "alertinfo" { + triggerInformation = item.Value + } else if item.Name == "type" { + triggerType = item.Value + } else if item.Name == "email" { + email = item.Value + } else if item.Name == "sms" { + sms = item.Value + } + } + + if len(triggerType) == 0 { + log.Printf("No type specified for user input node") + return errors.New("No type specified for user input node") + } + + // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms + ctx := context.Background() + startNode := trigger.ID + if strings.Contains(triggerType, "email") { + action := CloudSyncJob{ + Type: "user_input", + Action: "send_email", + OrgId: organizationId, + PrimaryItemId: workflowId, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: email, + FifthItem: referenceExecution, + } + + org, err := getOrg(ctx, organizationId) + if err != nil { + log.Printf("Failed email send to cloud: %s", err) + return err + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed email send to cloud", err) + return err + } + + log.Printf("Should send email to %s during execution.", email) + } + if strings.Contains(triggerType, "sms") { + action := CloudSyncJob{ + Type: "user_input", + Action: "send_sms", + OrgId: organizationId, + PrimaryItemId: workflowId, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: sms, + FifthItem: referenceExecution, + } + + org, err := getOrg(ctx, organizationId) + if err != nil { + log.Printf("Failed email send to cloud", err) + return err + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed email send to cloud", err) + return err + } + + log.Printf("Should send SMS to %s during execution.", sms) + } + + return nil +} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 2f10d25e..ba4d6199 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -478,7 +478,7 @@ const AngularWorkflow = (props) => { } } - if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE") { + if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status == "WAITING") { stop() setExecutionRunning(false) @@ -5195,12 +5195,12 @@ const AngularWorkflow = (props) => { />
- -
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ? @@ -5719,7 +5719,7 @@ const AngularWorkflow = (props) => { const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? "green" : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : "orange" const actionimg = curapp === null ? null : - {data.action.app_name} + {data.action.app_name} return (
diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 2520377e..412f0263 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -145,7 +145,21 @@ func deployWorker(image string, identifier string, env []string) { if err != nil { log.Printf("[ERROR] Container create error: %s", err) - return + + identifier := fmt.Sprintf("%s-new", identifier) + cont, err = dockercli.ContainerCreate( + context.Background(), + config, + hostConfig, + nil, + nil, + identifier, + ) + + if err != nil { + log.Printf("[ERROR] Container create error(2): %s", err) + return + } } err = dockercli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 3d2f9ebe..5872707f 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -2,7 +2,7 @@ NAME=worker VERSION=0.8.0 echo "Running docker build with $NAME:$VERSION" -CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . +#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION # Push both for now.. diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index c6b3889a..244ecc4b 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -236,10 +236,9 @@ type Action struct { AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` } -// Added environment for location to execute type Trigger struct { AppName string `json:"app_name" datastore:"app_name"` - Description string `json:"description" datastore:"description"` + Description string `json:"description" datastore:"description,noindex"` LongDescription string `json:"long_description" datastore:"long_description"` Status string `json:"status" datastore:"status"` AppVersion string `json:"app_version" datastore:"app_version"` @@ -253,6 +252,7 @@ type Trigger struct { Environment string `json:"environment" datastore:"environment"` TriggerType string `json:"trigger_type" datastore:"trigger_type"` Name string `json:"name" datastore:"name"` + Tags []string `json:"tags" datastore:"tags" yaml:"tags"` Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` Position struct { X float64 `json:"x" datastore:"x"` @@ -549,6 +549,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // source = parent node, dest = child node // parent can have more children, child can have more parents + extra := 0 for _, branch := range workflowExecution.Workflow.Branches { // Check what the parent is first. If it's trigger - skip sourceFound := false @@ -563,6 +564,21 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } } + for _, trigger := range workflowExecution.Workflow.Triggers { + if trigger.AppName != "User Input" { + continue + } + + if trigger.ID == branch.SourceID { + sourceFound = true + extra += 1 + } + + if trigger.ID == branch.DestinationID { + destinationFound = true + } + } + if sourceFound { parents[branch.DestinationID] = append(parents[branch.DestinationID], branch.SourceID) } else { @@ -576,7 +592,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } } - log.Printf("Actions: %d", len(workflowExecution.Workflow.Actions)) + log.Printf("Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) for _, action := range workflowExecution.Workflow.Actions { if action.Environment != environment { continue @@ -775,7 +791,41 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // IF NOT VISITED && IN toExecuteOnPrem // SKIP if it's not onprem for _, nextAction := range nextActions { - action := getAction(workflowExecution, nextAction) + action := getAction(workflowExecution, nextAction, environment) + if action.AppName == "User Input" { + log.Printf("USER INPUT!") + + if action.ID == workflowExecution.Start { + log.Printf("Skipping because it's the startnode") + } else { + log.Printf("Should stop after this iteration because it's user-input based. %#v", action) + trigger := Trigger{} + for _, innertrigger := range workflowExecution.Workflow.Triggers { + if innertrigger.ID == action.ID { + trigger = innertrigger + break + } + } + + trigger.LargeImage = "" + triggerData, err := json.Marshal(trigger) + if err != nil { + log.Printf("Failed unmarshalling action: %s", err) + triggerData = []byte("Failed unmarshalling. Cancel execution!") + } + + err = runUserInput(client, action, workflowExecution.Workflow.ID, workflowExecution.ExecutionId, workflowExecution.Authorization, string(triggerData)) + if err != nil { + log.Printf("Failed launching backend magic: %s", err) + os.Exit(3) + } else { + log.Printf("Launched user input node succesfully!") + os.Exit(3) + } + + break + } + } // check visited and onprem if arrayContains(visited, nextAction) { @@ -911,7 +961,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // https://devblogs.microsoft.com/oldnewthing/20100203-00/?p=15083 maxSize := 32700 - len(string(actionData)) - 2000 if len(executionData) < maxSize { - log.Printf("ADDING FULL_EXECUTION because size is larger than %d", maxSize) + log.Printf("ADDING FULL_EXECUTION because size is smaller than %d", maxSize) env = append(env, fmt.Sprintf("FULL_EXECUTION=%s", string(executionData))) } else { log.Printf("Skipping FULL_EXECUTION because size is larger than %d", maxSize) @@ -973,7 +1023,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } - log.Printf("Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + log.Printf("Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra) if workflowExecution.Status != "EXECUTING" { log.Printf("Exiting as worker execution has status %s!", workflowExecution.Status) shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) @@ -1075,16 +1125,73 @@ func getResult(workflowExecution WorkflowExecution, id string) ActionResult { return ActionResult{} } -func getAction(workflowExecution WorkflowExecution, id string) Action { +func getAction(workflowExecution WorkflowExecution, id, environment string) Action { for _, action := range workflowExecution.Workflow.Actions { if action.ID == id { return action } } + for _, trigger := range workflowExecution.Workflow.Triggers { + if trigger.ID == id { + return Action{ + ID: trigger.ID, + AppName: trigger.AppName, + Name: trigger.AppName, + Environment: environment, + } + log.Printf("FOUND TRIGGER: %#v!", trigger) + } + } + return Action{} } +func runUserInput(client *http.Client, action Action, workflowId, workflowExecutionId, authorization string, configuration string) error { + timeNow := time.Now().Unix() + result := ActionResult{ + Action: action, + ExecutionId: workflowExecutionId, + Authorization: authorization, + Result: configuration, + StartedAt: timeNow, + CompletedAt: 0, + Status: "WAITING", + } + + resultData, err := json.Marshal(result) + if err != nil { + return err + } + + fullUrl := fmt.Sprintf("%s/api/v1/streams", baseUrl) + req, err := http.NewRequest( + "POST", + fullUrl, + bytes.NewBuffer([]byte(resultData)), + ) + + if err != nil { + log.Printf("Error building test request: %s", err) + return err + } + + newresp, err := client.Do(req) + if err != nil { + log.Printf("Error running test request: %s", err) + return err + } + + body, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("Failed reading body when waiting: %s", err) + return err + } + + log.Printf("[INFO] Body: %s", string(body)) + return nil +} + func runTestExecution(client *http.Client, workflowId, apikey string) (string, string) { fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", baseUrl, workflowId) req, err := http.NewRequest( @@ -1173,7 +1280,6 @@ func main() { shutdown(executionId, "") } - // FIXME - tmp data := fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, executionId, authorization) fullUrl := fmt.Sprintf("%s/api/v1/streams/results", baseUrl) req, err := http.NewRequest( From c7a626bdbd4e5268d0186f6815fd262e9f231a9f Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 17 Nov 2020 15:54:42 +0100 Subject: [PATCH 15/18] Migrated docker images to be versioned --- .env | 6 ++-- backend/app_sdk/build.sh | 4 +-- backend/go-app/walkoff.go | 32 +++++++++++++---- docker-compose.yml | 12 +++---- frontend/src/views/AngularWorkflow.jsx | 11 +++--- functions/onprem/orborus/Dockerfile | 3 +- functions/onprem/orborus/build.sh | 8 ++--- functions/onprem/orborus/orborus.go | 49 ++++++++++++++++---------- functions/onprem/worker/build.sh | 4 +-- functions/onprem/worker/worker.go | 19 +++++----- 10 files changed, 93 insertions(+), 55 deletions(-) diff --git a/.env b/.env index 4fd9eced..93b05920 100644 --- a/.env +++ b/.env @@ -35,6 +35,6 @@ SHUFFLE_HTTP_PROXY= SHUFFLE_HTTPS_PROXY= SHUFFLE_PASS_WORKER_PROXY=TRUE -SHUFFLE_BASE_IMAGE_REGISTRY=docker.io -SHUFFLE_BASE_IMAGE_NAME=frikky/shuffle -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.6.0" \ No newline at end of file +SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io +SHUFFLE_BASE_IMAGE_NAME=frikky +SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.0" diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index bfd63f22..5e2fc3aa 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -NAME=app_sdk -VERSION=0.7.6 +NAME=shuffle-app_sdk +VERSION=0.8.0 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:$NAME -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 729fbdd8..08f8846b 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -919,6 +919,16 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId) if err != nil { log.Printf("Failed userinput handler: %s", err) + actionResult.Result = fmt.Sprintf("Cloud error: %s", err) + workflowExecution.Results = append(workflowExecution.Results, actionResult) + workflowExecution.Status = "ABORTED" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + log.Printf("Failed ") + } else { + log.Printf("Successfully set the execution to waiting.") + } + resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err))) } else { @@ -1311,7 +1321,7 @@ func handleExecutionStatistics(execution WorkflowExecution) { log.Printf("Added %d exampleresults to backend", successful) } else { - log.Printf("No examplresults necessary to be added for execution %s", execution.ExecutionId) + log.Printf("No example results necessary to be added for execution %s", execution.ExecutionId) } } @@ -2756,7 +2766,17 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // If the node is NOT found, it's supposed to be set to SKIPPED, // as it's not a childnode of the startnode // This is a configuration item for the workflow itself. - if !workflowExecution.Workflow.Configuration.StartFromTop { + if len(workflowExecution.Results) > 0 { + defaultResults = []ActionResult{} + for _, result := range workflowExecution.Results { + if result.Status == "WAITING" { + result.Status = "FINISHED" + result.Result = "Continuing" + } + + defaultResults = append(defaultResults, result) + } + } else if len(workflowExecution.Results) == 0 && !workflowExecution.Workflow.Configuration.StartFromTop { found := false for _, nodeId := range childNodes { if nodeId == action.ID { @@ -6167,13 +6187,13 @@ func handleUserInput(trigger Trigger, organizationId string, workflowId string, org, err := getOrg(ctx, organizationId) if err != nil { - log.Printf("Failed email send to cloud: %s", err) + log.Printf("Failed email send to cloud (1): %s", err) return err } err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { - log.Printf("Failed email send to cloud", err) + log.Printf("Failed email send to cloud (2): %s", err) return err } @@ -6193,13 +6213,13 @@ func handleUserInput(trigger Trigger, organizationId string, workflowId string, org, err := getOrg(ctx, organizationId) if err != nil { - log.Printf("Failed email send to cloud", err) + log.Printf("Failed sms send to cloud (3): %s", err) return err } err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { - log.Printf("Failed email send to cloud", err) + log.Printf("Failed sms send to cloud (4): %s", err) return err } diff --git a/docker-compose.yml b/docker-compose.yml index 37e61122..f57f1e90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: '3' services: frontend: - #build: ./frontend - image: frikky/shuffle:frontend + build: ./frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.0 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: build: ./backend - image: frikky/shuffle:backend + image: ghcr.io/frikky/shuffle-backend:0.8.0 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -43,7 +43,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: frikky/shuffle:orborus + image: ghcr.io/frikky/shuffle-orborus:0.8.0 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -51,8 +51,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - SHUFFLE_APP_SDK_VERSION=0.6.0 - - SHUFFLE_WORKER_VERSION=0.6.0 + - SHUFFLE_APP_SDK_VERSION=0.8.0 + - SHUFFLE_WORKER_VERSION=0.8.0 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index ba4d6199..3e417534 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1074,10 +1074,13 @@ const AngularWorkflow = (props) => { // FIXME - do this for both actions and other types? targetnode = workflow.triggers.findIndex(data => data.id === edge.target) if (targetnode !== -1) { - alert.error("Can't have triggers as target of branch") - event.target.remove() - found = true - break + if (workflow.triggers[targetnode].app_name !== "User Input") { + + alert.error("Can't have triggers as target of branch") + event.target.remove() + found = true + break + } } } } diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile index e5bc379e..3db4c027 100644 --- a/functions/onprem/orborus/Dockerfile +++ b/functions/onprem/orborus/Dockerfile @@ -5,7 +5,8 @@ WORKDIR /app RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client COPY orborus.go /app/orborus.go - +RUN go mod init orborus +RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o orborus . FROM alpine:3.12 diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 9c139d11..14499755 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,11 +1,11 @@ -NAME=orborus -VERSION=0.6.2 +NAME=shuffle-orborus +VERSION=0.8.0 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION #docker push frikky/$NAME:$VERSION -docker push frikky/shuffle:$NAME +#docker push frikky/shuffle:$NAME # docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -#docker push ghcr.io/frikky/$NAME:$VERSION +docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 412f0263..78984425 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" dockerclient "github.com/docker/docker/client" + "github.com/satori/go.uuid" //network "github.com/docker/docker/api/types/network" //natting "github.com/docker/go-connections/nat" ) @@ -134,30 +135,34 @@ func deployWorker(image string, identifier string, env []string) { Env: env, } + log.Printf("Identifier: %s", identifier) cont, err := dockercli.ContainerCreate( context.Background(), config, hostConfig, nil, - nil, identifier, ) if err != nil { - log.Printf("[ERROR] Container create error: %s", err) + if strings.Contains(fmt.Sprintf("%s", err), "Conflict. The container name ") { + uuid := uuid.NewV4() + identifier = fmt.Sprintf("%s-%s", identifier, uuid) + log.Printf("2 - Identifier: %s", identifier) + cont, err = dockercli.ContainerCreate( + context.Background(), + config, + hostConfig, + nil, + identifier, + ) - identifier := fmt.Sprintf("%s-new", identifier) - cont, err = dockercli.ContainerCreate( - context.Background(), - config, - hostConfig, - nil, - nil, - identifier, - ) - - if err != nil { - log.Printf("[ERROR] Container create error(2): %s", err) + if err != nil { + log.Printf("[ERROR] Container create error(2): %s", err) + return + } + } else { + log.Printf("[ERROR] Container create error: %s", err) return } } @@ -222,27 +227,32 @@ func initializeImages() { ctx := context.Background() if appSdkVersion == "" { - appSdkVersion = "0.6.0" + appSdkVersion = "0.8.0" log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.6.0" + workerVersion = "0.8.0" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } if baseimageregistry == "" { baseimageregistry = "docker.io" + baseimageregistry = "ghcr.io" log.Printf("Setting baseimageregistry") } if baseimagename == "" { baseimagename = "frikky/shuffle" + baseimagename = "frikky" log.Printf("Setting baseimagename") } // check whether they are the same first images := []string{ - fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), - fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix), + //fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), + //fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix), + + fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", baseimageregistry, baseimagename, appSdkVersion), + fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion), // fmt.Sprintf("docker.io/%s:app_sdk", baseimagename), // fmt.Sprintf("docker.io/%s:worker", baseimagename), @@ -315,7 +325,8 @@ func main() { //workerImage := fmt.Sprintf("%s/worker:%s", baseimagename, workerVersion) // workerImage := fmt.Sprintf("docker.io/%s:worker", baseimagename) // fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), - workerImage := fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix) + //workerImage := fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix) + workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion) log.Printf("[INFO] Finished configuring docker environment") diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 5872707f..363df000 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,4 +1,4 @@ -NAME=worker +NAME=shuffle-worker VERSION=0.8.0 echo "Running docker build with $NAME:$VERSION" @@ -9,4 +9,4 @@ docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$ #docker push frikky/$NAME:$VERSION docker push frikky/shuffle:$NAME #docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -#docker push ghcr.io/frikky/$NAME:$VERSION +docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 244ecc4b..dbaea55f 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -792,11 +792,20 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // SKIP if it's not onprem for _, nextAction := range nextActions { action := getAction(workflowExecution, nextAction, environment) + // check visited and onprem + if arrayContains(visited, nextAction) { + log.Printf("ALREADY VISITIED (%s): %s", action.Label, nextAction) + continue + } + if action.AppName == "User Input" { log.Printf("USER INPUT!") if action.ID == workflowExecution.Start { log.Printf("Skipping because it's the startnode") + visited = append(visited, action.ID) + executed = append(executed, action.ID) + continue } else { log.Printf("Should stop after this iteration because it's user-input based. %#v", action) trigger := Trigger{} @@ -827,12 +836,6 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } } - // check visited and onprem - if arrayContains(visited, nextAction) { - log.Printf("ALREADY VISITIED (%s): %s", action.Label, nextAction) - continue - } - // Not really sure how this edgecase happens. // FIXME @@ -866,7 +869,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } if continueOuter { - //log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) + log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) //for _, tmpaction := range parents[nextAction] { // action := getAction(workflowExecution, tmpaction) // _ = action @@ -1029,7 +1032,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } - if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions) { + if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { shutdownCheck := true ctx := context.Background() for _, result := range workflowExecution.Results { From 59ef35361a5a98aa3a4f9dbeb2a615851bf1fe8e Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 17 Nov 2020 15:57:28 +0100 Subject: [PATCH 16/18] Fixed docker compose --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f57f1e90..05d3e546 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - build: ./frontend + #build: ./frontend image: ghcr.io/frikky/shuffle-frontend:0.8.0 container_name: shuffle-frontend hostname: shuffle-frontend @@ -16,7 +16,7 @@ services: depends_on: - backend backend: - build: ./backend + #build: ./backend image: ghcr.io/frikky/shuffle-backend:0.8.0 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} From d297a7b4793d1970ef97aa445305e3e8a6709c92 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 20 Nov 2020 04:00:29 +0100 Subject: [PATCH 17/18] Last small fixes for 0.8 --- backend/go-app/main.go | 38 +--- backend/go-app/walkoff.go | 19 +- frontend/src/views/Admin.jsx | 283 +++++++------------------ frontend/src/views/AngularWorkflow.jsx | 11 +- frontend/src/views/Docs.jsx | 8 +- 5 files changed, 105 insertions(+), 254 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 687016e6..ff2685c4 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -867,12 +867,6 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { userId = location[4] } - if userId == userInfo.Id { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Can't deactivate yourself"}`)) - return - } - ctx := context.Background() foundUser, err := getUser(ctx, userId) if err != nil { @@ -904,6 +898,7 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { // Invert. No user deletion. if foundUser.Active { + foundUser.Active = false } else { foundUser.Active = true @@ -1180,6 +1175,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { ID := uuid.NewV4() newUser.Id = ID.String() newUser.VerificationToken = verifyToken.String() + err = setUser(ctx, newUser) if err != nil { log.Printf("Error adding User %s: %s", username, err) @@ -1187,10 +1183,8 @@ func createNewUser(username, password, role, apikey string, org Org) error { } neworg, err := getOrg(ctx, org.Id) - log.Printf("Users: %d", len(org.Users)) if err == nil { - log.Printf("Updating org %s with user %s", org.Name, newUser.Username) - neworg.Users = append(org.Users, *newUser) + neworg.Users = append(neworg.Users, *newUser) err = setOrg(ctx, *neworg, neworg.Id) if err != nil { log.Printf("Failed updating org with user %s", newUser.Username) @@ -1199,26 +1193,6 @@ func createNewUser(username, password, role, apikey string, org Org) error { } } - // url := fmt.Sprintf("https://shuffler.io/register/%s", verifyToken.String()) - // const verifyMessage = ` - //Registration URL :) - // - //%s - // ` - // addr := newUser.Username - // - // msg := &mail.Message{ - // Sender: "Shuffle ", - // To: []string{addr}, - // Subject: "Verify your username - Shuffle", - // Body: fmt.Sprintf(verifyMessage, url), - // } - // - // log.Println(msg.Body) - // if err := mail.Send(ctx, msg); err != nil { - // log.Printf("Couldn't send email: %v", err) - // } - err = increaseStatisticsField(ctx, "successful_register", username, 1) if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) @@ -2462,6 +2436,7 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) { item.Password = "" item.Session = "" item.VerificationToken = "" + item.Orgs = []string{} newUsers = append(newUsers, item) } @@ -2808,7 +2783,6 @@ func fixUserOrg(ctx context.Context, user *User) *User { user.Orgs = append(user.Orgs, user.ActiveOrg.Id) } - log.Printf("Updating %d orgs for user %s", len(user.Orgs), user.Id) // Might be vulnerable to timing attacks. for _, orgId := range user.Orgs { if len(orgId) == 0 { @@ -2821,8 +2795,6 @@ func fixUserOrg(ctx context.Context, user *User) *User { continue } - log.Printf("Org users: %d", len(org.Users)) - orgIndex := 0 userFound := false for index, orgUser := range org.Users { @@ -2844,7 +2816,6 @@ func fixUserOrg(ctx context.Context, user *User) *User { org.Users = append(org.Users, *user) } - log.Printf("Users 2: %d", len(org.Users)) err = setOrg(ctx, *org, orgId) if err != nil { log.Printf("Failed setting org %s", orgId) @@ -7719,6 +7690,7 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { return } + log.Printf("SUCCESSFULLY UPDATED ORG") resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully updated org"}`))) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 08f8846b..1bd98563 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -73,6 +73,8 @@ type SyncFeatures struct { UserInput SyncData `json:"user_input" datastore:"user_input"` SendMail SyncData `json:"send_mail" datastore:"send_mail"` SendSms SyncData `json:"send_sms" datastore:"send_sms"` + Updates SyncData `json:"updates" datastore:"updates"` + Notifications SyncData `json:"notifications" datastore:"notifications"` EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` @@ -84,13 +86,14 @@ type SyncFeatures struct { } type SyncData struct { - Active bool `json:"active" datastore:"active"` - Type string `json:"type" datastore:"type"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - Limit int64 `json:"limit" datastore:"limit"` - StartDate int64 `json:"start_date" datastore:"start_date"` - EndDate int64 `json:"end_date" datastore:"end_date"` + Active bool `json:"active" datastore:"active"` + Type string `json:"type" datastore:"type"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Limit int64 `json:"limit" datastore:"limit"` + StartDate int64 `json:"start_date" datastore:"start_date"` + EndDate int64 `json:"end_date" datastore:"end_date"` + DataCollection int64 `json:"data_collection" datastore:"data_collection"` } type SyncConfig struct { @@ -1193,7 +1196,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // Handles extra statistics stuff when it's done // Does autocomplete magic with JSON - go handleExecutionStatistics(*workflowExecution) + handleExecutionStatistics(*workflowExecution) } } diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 25b9ead8..ad857ad6 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -29,6 +29,7 @@ import { useAlert } from "react-alert"; import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core'; import { useTheme } from '@material-ui/core/styles'; import HandlePayment from './HandlePayment' +import OrgHeader from '../components/OrgHeader' import PolymerIcon from '@material-ui/icons/Polymer'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; @@ -44,7 +45,6 @@ import ScheduleIcon from '@material-ui/icons/Schedule'; import CloudIcon from '@material-ui/icons/Cloud'; import BusinessIcon from '@material-ui/icons/Business'; -const defaultImage = "data:image/png;base64,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" const useStyles = makeStyles({ notchedOutline: { @@ -61,8 +61,6 @@ const Admin = (props) => { const [firstRequest, setFirstRequest] = React.useState(true); const [modalUser, setModalUser] = React.useState({}); const [modalOpen, setModalOpen] = React.useState(false); - const [file, setFile] = React.useState(""); - const [fileBase64, setFileBase64] = React.useState(""); const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false); const [cloudSyncApikey, setCloudSyncApikey] = React.useState(""); @@ -76,8 +74,6 @@ const Admin = (props) => { const [organizations, setOrganizations] = React.useState([]); const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); const [userSettings, setUserSettings] = React.useState({}); - const [orgName, setOrgName] = React.useState(""); - const [orgDescription, setOrgDescription] = React.useState(""); const [environments, setEnvironments] = React.useState([]); const [authentication, setAuthentication] = React.useState([]); @@ -257,8 +253,10 @@ const Admin = (props) => { getOrgs() if (disableSync) { alert.success("Successfully disabled sync!") + setOrgSyncResponse("Successfully disabled syncronization") } else { alert.success("Cloud Syncronization successfully set up!") + setOrgSyncResponse("Successfully started syncronization. Cloud features you now have access to can be seen below.") } selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync @@ -274,50 +272,9 @@ const Admin = (props) => { }) } - const orgSaveButton = - - const handleEditOrg = (name, description, orgId, image) => { - const data = { - "name": name, - "description": description, - "org_id": orgId, - "image": image, - } - - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; - fetch(url, { - mode: 'cors', - method: 'POST', - body: JSON.stringify(data), - credentials: 'include', - crossDomain: true, - withCredentials: true, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }) - .then(response => - response.json().then(responseJson => { - if (responseJson["success"] === false) { - alert.error("Failed updating org: ", responseJson.reason) - } else { - alert.success("Successfully edited org!") - setSelectedUserModalOpen(false) - } - }), - ) - .catch(error => { - alert.error("Err: " + error.toString()) - }); - } + + const onPasswordChange = () => { const data = { "username": selectedUser.username, "newpassword": newPassword } @@ -420,11 +377,8 @@ const Admin = (props) => { //console.log(responseJson.sync_features[key]) }) - setOrgName(responseJson.name) - setOrgDescription(responseJson.description) - if (responseJson.image !== undefined && responseJson.image !== null && responseJson.image.length > 0) { - setFileBase64(responseJson.image) - } + //setOrgName(responseJson.name) + //setOrgDescription(responseJson.description) setOrganizationFeatures(lists) } }) @@ -614,6 +568,7 @@ const Admin = (props) => { return response.json() }) .then((responseJson) => { + console.log(responseJson) setSchedules(responseJson) }) .catch(error => { @@ -777,7 +732,7 @@ const Admin = (props) => { } if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined) { - setSelectedOrganization(userdata.active_org) + //setSelectedOrganization(userdata.active_org) handleGetOrg(userdata.active_org.id) } @@ -994,6 +949,8 @@ const Admin = (props) => { const GridItem = (props) => { + const [expanded, setExpanded] = React.useState(false) + const primary = props.data.primary const secondary = props.data.secondary const primaryIcon = props.data.icon @@ -1003,8 +960,10 @@ const Admin = (props) => { return ( - - + { + setExpanded(!expanded) + }}> + @@ -1012,11 +971,24 @@ const Admin = (props) => { {secondaryIcon} + {expanded ? +
+ + Usage: {props.data.limit === 0 ? "Infinite" : {props.data.usage} / {props.data.limit}} + + + Data sharing: {props.data.data_collection} + + + Description: {secondary} + +
+ : null}
) @@ -1061,7 +1033,7 @@ const Admin = (props) => { Enable cloud features - What does cloud sync do? + What does cloud sync do?
{ * Easily share apps and workflows
* Access to powerful cloud search - - - var image = "" - const editHeaderImage = (event) => { - const file = event.target.value - const actualFile = event.target.files[0] - const fileObject = URL.createObjectURL(actualFile) - setFile(fileObject) - } - - if (file !== "") { - const img = document.getElementById('logo') - var canvas = document.createElement('canvas') - canvas.width = 174 - canvas.height = 174 - var ctx = canvas.getContext('2d') - - img.onload = function() { - // img, x, y, width, height - //ctx.drawImage(img, 174, 174) - //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight) - //ctx.drawImage(img, 0, 0, 174, 174) - ctx.drawImage(img, - 0, 0, img.width, img.height, - 0, 0, canvas.width, canvas.height - ) - - const canvasUrl = canvas.toDataURL() - if (canvasUrl !== fileBase64) { - setFileBase64(canvasUrl) - selectedOrganization.image = canvasUrl - setSelectedOrganization(selectedOrganization) - } - } - } - - var imageData = file.length > 0 ? file : fileBase64 - imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData + const cancelSubscriptions = (subscription_id) => { console.log(selectedOrganization) @@ -1203,101 +1138,24 @@ const Admin = (props) => { }) } - const imageInfo = const organizationView = curTab === 0 && selectedOrganization.id !== undefined ?

Organization overview

- On this page you can configure individual parts of your organization. Learn more + On this page you can configure individual parts of your organization. Learn more
{selectedOrganization.id === undefined ?
:
-
- -
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> - upload = ref} onChange={editHeaderImage} /> - {imageInfo} -
-
-
-
- Name - { - const invalid = ["#", ":", "."] - for (var key in invalid) { - if (e.target.value.includes(invalid[key])) { - alert.error("Can't use "+invalid[key]+" in name") - return - } - } - - if (e.target.value.length > 100) { - alert.error("Choose a shorter name.") - return - } - - setOrgName(e.target.value) - }} - color="primary" - InputProps={{ - style:{ - color: "white", - height: "50px", - fontSize: "1em", - }, - classes: { - notchedOutline: classes.notchedOutline, - }, - }} - /> -
- Description -
- { - setOrgDescription(e.target.value) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style:{ - color: "white", - }, - }} - /> -
- {orgSaveButton} -
-
-
-
+ {selectedOrganization.name.length > 0 ? + + : null} Cloud syncronization - What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. + What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. {isCloud ?
@@ -1379,19 +1237,10 @@ const Admin = (props) => {
{orgSyncResponse.length > 0 ? - Error from Shuffle: {orgSyncResponse} + Message from Shuffle: {orgSyncResponse} : null } - {/* - - {syncList.map((data, index) => { - return ( - - ) - })} - - */}
} Cloud sync features @@ -1401,11 +1250,14 @@ const Admin = (props) => { return null } - // const item = selectedOrganization.sync_features[key] + const newkey = key.replace("_", " ") const griditem = { - "primary": key, - "secondary": "", + "primary": newkey, + "secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description, + "limit": item.limit, + "usage": 0, + "data_collection": "None", "active": item.active, "icon": , } @@ -1573,7 +1425,7 @@ const Admin = (props) => {

User management

- Add, edit, block or change passwords + Add, edit, block or change passwords. Learn more
- + + ) })} @@ -1672,41 +1536,49 @@ const Admin = (props) => {

Schedules

- Schedules used in Workflows. Makes locating and control easier. + Schedules used in Workflows. Makes locating and control easier. Learn more
+ - {schedules === undefined || schedules === null ? null : schedules.map(schedule => { + {schedules === undefined || schedules === null ? null : schedules.map((schedule, index) => { return ( - + {schedule.seconds} seconds} /> + {schedule.workflow_id}} + /> + + var imageData = file.length > 0 ? file : fileBase64 + imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData + const imageInfo = + return ( +
+
+ +
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> + upload = ref} onChange={editHeaderImage} /> + {imageInfo} +
+
+
+
+ Name + { + const invalid = ["#", ":", "."] + for (var key in invalid) { + if (e.target.value.includes(invalid[key])) { + alert.error("Can't use "+invalid[key]+" in name") + return + } + } + + if (e.target.value.length > 100) { + alert.error("Choose a shorter name.") + return + } + + setOrgName(e.target.value) + }} + color="primary" + InputProps={{ + style:{ + color: "white", + height: "50px", + fontSize: "1em", + }, + classes: { + notchedOutline: classes.notchedOutline, + }, + }} + /> +
+ Description +
+ { + setOrgDescription(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> +
+ {orgSaveButton} +
+
+
+
+
+ ) +} + +export default OrgHeader