From 7c33b91f1f86cac906fdedf4e80e0b6e9e30a36e Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 14 May 2020 08:48:20 +0200 Subject: [PATCH] Added backend statistics for dashboard --- README.md | 1 + backend/go-app/main.go | 105 ++++++++++++++++++++++++++++++-- backend/go-app/walkoff.go | 99 ++++++++++++++++++++++++++++++ backend/tests/hooks.sh | 27 ++++---- frontend/src/AngularWorkflow.js | 2 +- frontend/src/App.js | 8 +-- frontend/src/Dashboard.js | 76 +++++++++++++++++++++++ frontend/src/Header.js | 14 ----- frontend/src/Workflows.js | 18 +++--- 9 files changed, 297 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 1f1bbbd5..20d91e06 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Documentation can be found on https://shuffler.io/docs/about or in your own inst ## In the works * User run statistics - Dashboard * Debug view for manual executions +* App versioning ### Setup - Local Frontend - requires [npm](https://nodejs.org/en/download/)/[yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)/your preferred manager. Runs independently from backend - edit frontend/src/App.yaml to change from localhost to prod setting. diff --git a/backend/go-app/main.go b/backend/go-app/main.go index b702c10e..941f4c0e 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -79,6 +79,35 @@ type ExecutionInfo struct { DailyOnpremExecutions int64 `json:"daily_onprem_executions" datastore:"daily_onprem_executions"` } +type StatisticsData struct { + Timestamp int64 `json:"timestamp" datastore:"timestamp"` + Id string `json:"id" datastore:"id"` + Amount int64 `json:"amount" datastore:"amount"` +} + +type StatisticsItem struct { + Total int64 `json:"total" datastore:"total"` + Fieldname string `json:"field_name" datastore:"field_name"` + Data []StatisticsData `json:"data" datastore:"data"` +} + +// "Execution by status" +// Execution history +//type GlobalStatistics struct { +// BackendExecutions int64 `json:"backend_executions" datastore:"backend_executions"` +// WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` +// ExecutionCount int64 `json:"execution_count" datastore:"execution_count"` +// ExecutionSuccessCount int64 `json:"execution_success_count" datastore:"execution_success_count"` +// ExecutionAbortCount int64 `json:"execution_abort_count" datastore:"execution_abort_count"` +// ExecutionFailureCount int64 `json:"execution_failure_count" datastore:"execution_failure_count"` +// ExecutionPendingCount int64 `json:"execution_pending_count" datastore:"execution_pending_count"` +// AppUsageCount int64 `json:"app_usage_count" datastore:"app_usage_count"` +// TotalAppsCount int64 `json:"total_apps_count" datastore:"total_apps_count"` +// SelfMadeAppCount int64 `json:"self_made_app_count" datastore:"self_made_app_count"` +// WebhookUsageCount int64 `json:"webhook_usage_count" datastore:"webhook_usage_count"` +// Baseline map[string]int64 `json:"baseline" datastore:"baseline"` +//} + type ParsedOpenApi struct { Body string `datastore:"body,noindex" json:"body"` ID string `datastore:"id" json:"id"` @@ -2783,6 +2812,11 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { workflowExecution, executionResp, err := handleExecution(item, workflow, request) if err == nil { + err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) return @@ -4481,6 +4515,55 @@ func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) { resp.Write(b) } +func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + _, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in getting specific workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + + var statsId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + statsId = location[4] + } + + ctx := context.Background() + statisticsId := "global_statistics" + nameKey := statsId + key := datastore.NameKey(statisticsId, nameKey, nil) + statisticsItem := StatisticsItem{} + if err := dbclient.Get(ctx, key, &statisticsItem); err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + b, err := json.Marshal(statisticsItem) + if err != nil { + log.Println("Failed to marshal data: %s", err) + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(b)) +} + func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5503,6 +5586,15 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } setOpenApiDatastore(ctx, api.ID, parsed) + err = increaseStatisticsField(ctx, "total_apps_created", api.ID, 1) + if err != nil { + log.Printf("Failed to increase success execution stats: %s", err) + } + + err = increaseStatisticsField(ctx, "openapi_apps_created", api.ID, 1) + if err != nil { + log.Printf("Failed to increase success execution stats: %s", err) + } resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) @@ -5522,6 +5614,11 @@ func init() { log.Fatalf("DBclient error during init: %s", err) } + err = increaseStatisticsField(ctx, "backend_executions", "", 1) + if err != nil { + log.Printf("Failed increasing local stats: %s", err) + } + count, err := getEnvironmentCount() if count == 0 && err == nil { item := Environment{ @@ -5554,15 +5651,9 @@ func init() { r.HandleFunc("/api/v1/getenvironments", handleGetEnvironments).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/setenvironments", handleSetEnvironments).Methods("PUT", "OPTIONS") - //r.HandleFunc("/api/v1/register/{key}", handleRegisterVerification).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/getinfo", handleInfo).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/getsettings", handleSettings).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/generateapikey", handleApiGeneration).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/passwordchange", handlePasswordChange).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/passwordresetmail", handlePasswordResetMail).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/passwordreset", handlePasswordReset).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/contact", handleContact).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/docs", getDocList).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/docs/{key}", getDocs).Methods("GET", "OPTIONS") @@ -5614,6 +5705,8 @@ func init() { // Trigger hmm r.HandleFunc("/api/v1/triggers/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") + // OpenAPI configuration r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/verify_openapi", verifySwagger).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index bf35e344..4d0f02cb 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -274,6 +274,62 @@ type ExecutionRequestWrapper struct { Data []ExecutionRequest `json:"data"` } +// This might be... a bit off, but that's fine :) +// This might also be stupid, as we want timelines and such +// Anyway, these are super basic stupid stats. +func increaseStatisticsField(ctx context.Context, fieldname, id string, amount int64) error { + + // 1. Get current stats + // 2. Increase field(s) + // 3. Put new stats + statisticsId := "global_statistics" + nameKey := fieldname + key := datastore.NameKey(statisticsId, nameKey, nil) + + statisticsItem := StatisticsItem{} + newData := StatisticsData{ + Timestamp: int64(time.Now().Unix()), + Amount: amount, + Id: id, + } + + if err := dbclient.Get(ctx, key, &statisticsItem); err != nil { + // Should init + if strings.Contains(fmt.Sprintf("%s", err), "entity") { + statisticsItem = StatisticsItem{ + Total: amount, + Fieldname: fieldname, + Data: []StatisticsData{ + newData, + }, + } + + if _, err := dbclient.Put(ctx, key, &statisticsItem); err != nil { + log.Printf("Error setting base stats: %s", err) + return err + } + + return nil + } + //log.Printf("STATSERR: %s", err) + + return err + } + + statisticsItem.Total += amount + statisticsItem.Data = append(statisticsItem.Data, newData) + + // New struct, to not add body, author etc + if _, err := dbclient.Put(ctx, key, &statisticsItem); err != nil { + log.Printf("Error stats to %s: %s", fieldname, err) + return err + } + + log.Printf("Stats: %#v", statisticsItem) + + return nil +} + func setWorkflowQueue(ctx context.Context, executionRequests ExecutionRequestWrapper, id string) error { key := datastore.NameKey("workflowqueue", id, nil) @@ -607,6 +663,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { if workflowExecution.Status == "FINISHED" { log.Printf("Workflowexecution is already FINISHED. No further action can be taken") + resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because of %s with status %s"}`, workflowExecution.LastNode, workflowExecution.Status))) return @@ -615,6 +672,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // Not sure what's up here // FIXME - remove comment if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { + log.Printf("Workflowexecution is already aborted. No further action can be taken") resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status))) @@ -645,6 +703,18 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { workflowExecution.Result = lastResult workflowExecution.Results = newResults + + if workflowExecution.Status == "ABORTED" { + err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase aborted execution stats: %s", err) + } + } else if workflowExecution.Status == "FAILURE" { + err = increaseStatisticsField(ctx, "workflow_executions_failure", workflowExecution.Workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase failure execution stats: %s", err) + } + } } // This means it should continue I think :) @@ -722,6 +792,11 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { if workflowExecution.LastNode == "" { workflowExecution.LastNode = actionResult.Action.ID } + + err = increaseStatisticsField(ctx, "workflow_executions_success", workflowExecution.Workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase success execution stats: %s", err) + } } } @@ -1491,6 +1566,11 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { return } + err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase aborted execution stats: %s", err) + } + // FIXME - allowed to edit it? idk resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) @@ -1834,6 +1914,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } + err = increaseStatisticsField(ctx, "workflow_executions", workflow.ID, 1) + if err != nil { + log.Printf("Failed to increase stats execution stats: %s", err) + } + return workflowExecution, "", nil } @@ -2460,6 +2545,10 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { } } + err = increaseStatisticsField(ctx, "total_apps_deleted", fileId, 1) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } //err = memcache.Delete(request.Context(), sessionToken) resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) @@ -3043,6 +3132,16 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin return err } + err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1) + if err != nil { + log.Printf("Failed to increase total apps created stats: %s", err) + } + + err = increaseStatisticsField(ctx, "total_apps_loaded", workflowapp.ID, 1) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + log.Printf("Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) //memcache.Delete(ctx, "all_apps") //os.Exit(3) diff --git a/backend/tests/hooks.sh b/backend/tests/hooks.sh index 3a82ff61..4689fed9 100644 --- a/backend/tests/hooks.sh +++ b/backend/tests/hooks.sh @@ -23,19 +23,14 @@ #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_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -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:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 7861ed75..775a9ab4 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -4016,7 +4016,7 @@ const AngularWorkflow = (props) => { }, }} color="secondary" - placeholder={"Optional argument"} + placeholder={"Execution Argument"} onBlur={(e) => { setExecutionText(e.target.value) }} diff --git a/frontend/src/App.js b/frontend/src/App.js index 2ff6cc37..720ae0d8 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -76,14 +76,8 @@ const App = (message, props) => { if (dataset === false) { checkLogin() setDataset(true) - initializeReactGA() - } - }) + }}) - function initializeReactGA() { - } - - console.log(window.location) if (isLoaded && !isLoggedIn && (!window.location.pathname.startsWith("/login") && (!window.location.pathname.startsWith("/docs") && (!window.location.pathname.startsWith("/adminsetup"))))) { window.location = "login" } diff --git a/frontend/src/Dashboard.js b/frontend/src/Dashboard.js index 30e093b1..3082e3f6 100644 --- a/frontend/src/Dashboard.js +++ b/frontend/src/Dashboard.js @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import classNames from "classnames"; // react plugin used to create charts import { Line, Bar } from "react-chartjs-2"; +import { useAlert } from "react-alert"; // https://demos.creative-tim.com/black-dashboard-react/?ref=appseed#/admin/dashboard @@ -38,12 +39,87 @@ import { // This is the start of a dashboard that can be used. // What data do we fill in here? Idk const Dashboard = (props) => { + const { globalUrl } = props; + const alert = useAlert() const [bigChartData, setBgChartData] = useState("data1"); + const [firstRequest, setFirstRequest] = useState(true); + const [stats, setStats] = useState({}) + const [changeme, setChangeme] = useState("") document.title = "Shuffle - dashboard" + const fetchdata = (stats_id) => { + fetch(globalUrl+"/api/v1/stats/"+stats_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!") + } + + return response.json() + }) + .then((responseJson) => { + console.log("DATA: ", responseJson) + stats[stats_id] = responseJson + setStats(stats) + // Used to force updates + setChangeme(stats_id) + }) + .catch(error => { + alert.error("ERROR: "+error.toString()) + }); + } + + const variables = [ + "backend_executions", + "workflow_executions", + "workflow_executions_aborted", + "workflow_executions_success", + "total_apps_created", + "total_apps_loaded", + "openapi_apps_created", + "total_apps_deleted", + "total_webhooks_ran", + ] + + if (firstRequest) { + setFirstRequest(false) + + for (var key in variables) { + fetchdata(variables[key]) + } + } + + + const newdata = Object.getOwnPropertyNames(stats).length > 0 ? +
+ {variables.map(data => { + if (stats[data] === undefined || stats[data] === null) { + return null + } + + if (stats[data].total === undefined) { + return null + } + + return ( +
+ {data}: {stats[data].total} +
+ ) + })} +
+ : null + const data =
+ {newdata} diff --git a/frontend/src/Header.js b/frontend/src/Header.js index d8345961..7a01cb53 100644 --- a/frontend/src/Header.js +++ b/frontend/src/Header.js @@ -103,20 +103,6 @@ const Header = props => { const loginTextBrowser = !isLoggedIn ?
- - -
- - - - - - Shuffle - - -
- -
diff --git a/frontend/src/Workflows.js b/frontend/src/Workflows.js index 4b9a0174..209741f5 100644 --- a/frontend/src/Workflows.js +++ b/frontend/src/Workflows.js @@ -357,15 +357,15 @@ const Workflows = (props) => { > - { - setOpen(false) - setAnchorEl(null) - }} + { + setOpen(false) + setAnchorEl(null) + }} > {