Merge pull request #200 from frikky/launch

0.8.0 release
This commit is contained in:
Frikky
2020-11-20 09:19:36 +01:00
committed by GitHub
23 changed files with 2417 additions and 451 deletions
+3 -3
View File
@@ -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"
SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io
SHUFFLE_BASE_IMAGE_NAME=frikky
SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.0"
+4 -4
View File
@@ -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):
+2 -2
View File
@@ -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
+48 -46
View File
@@ -397,10 +397,12 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
verifyAddin,
)
if strings.Contains(functionname, "api_dumps_delete") {
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
@@ -1031,6 +1033,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 +1067,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 +1167,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 +1201,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 +1302,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 +1335,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 +1435,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 +1469,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 +1568,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 +1601,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 +1701,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 +1734,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 +1834,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 +1868,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)
}
}
}
+757 -113
View File
File diff suppressed because it is too large Load Diff
+433 -39
View File
@@ -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{
@@ -55,27 +56,44 @@ var scheduledJobs = 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 {
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"`
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"`
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 {
Active bool `json:"active" datastore:"active"`
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 {
@@ -86,6 +104,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,noindex"`
Id string `json:"id" datastore:"id"`
Org string `json:"org" datastore:"org"`
Users []User `json:"users" datastore:"users"`
@@ -307,6 +327,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 {
@@ -329,6 +350,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"`
@@ -341,6 +363,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 {
@@ -559,6 +582,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
LastModificationtime: timeNow,
LastRuntime: timeNow,
Org: orgId,
Environment: "onprem",
}
err = setSchedule(ctx, schedule)
@@ -664,7 +688,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?
@@ -878,6 +902,58 @@ 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)
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 {
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)
@@ -956,7 +1032,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 {
@@ -1120,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)
}
}
@@ -1180,10 +1256,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!")
}
@@ -1248,7 +1324,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)
}
}
@@ -1283,7 +1359,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 +1439,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)
@@ -1447,7 +1524,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
@@ -1779,6 +1876,53 @@ 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
}
}
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") {
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
}
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
}
log.Printf("Should send SMS to %s during execution.", sms)
}
}
//log.Println("TRIGGERS")
@@ -2201,7 +2345,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" {
@@ -2222,7 +2366,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 {
@@ -2301,7 +2445,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) {
@@ -2362,6 +2506,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 {
@@ -2624,7 +2769,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 {
@@ -2652,9 +2807,19 @@ 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))
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
@@ -3011,10 +3176,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}`)))
@@ -3292,6 +3508,68 @@ 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,
FifthItem: startNode,
}
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)
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(
@@ -3501,9 +3779,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 +3791,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 +3934,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 +4111,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 +4180,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)
@@ -4982,7 +5260,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
@@ -5467,9 +5745,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 {
@@ -5655,7 +5937,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}`))
@@ -5678,6 +5960,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)
@@ -5837,3 +6149,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 (1): %s", err)
return err
}
err = executeCloudAction(action, org.SyncConfig.Apikey)
if err != nil {
log.Printf("Failed email send to cloud (2): %s", 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 sms send to cloud (3): %s", err)
return err
}
err = executeCloudAction(action, org.SyncConfig.Apikey)
if err != nil {
log.Printf("Failed sms send to cloud (4): %s", err)
return err
}
log.Printf("Should send SMS to %s during execution.", sms)
}
return nil
}
+1 -11
View File
@@ -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"}}'
+6 -8
View File
@@ -2,7 +2,7 @@ version: '3'
services:
frontend:
#build: ./frontend
image: frikky/shuffle:frontend
image: ghcr.io/frikky/shuffle-frontend:0.8.0
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -16,8 +16,8 @@ services:
depends_on:
- backend
backend:
build: ./backend
image: frikky/shuffle:backend
#build: ./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,10 +51,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SHUFFLE_APP_SDK_VERSION=0.6.0
- SHUFFLE_APP_SDK_KALI_VERSION=0.6.0
- SHUFFLE_APP_SDK_BLACKARCH_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}
+5 -4
View File
@@ -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
@@ -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({
@@ -99,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"]) {
@@ -116,8 +117,8 @@ 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,
position: positions.BOTTOM_CENTER
timeout: 9000,
position: positions.BOTTOM_LEFT,
};
const includedData = window.location.pathname === "/home" || window.location.pathname === "/features" ?
+14 -13
View File
@@ -1,22 +1,23 @@
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: '10px',
padding: 15,
textTransform: 'uppercase',
borderRadius: '3px',
display: 'flex',
justifyContent: 'space-between',
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: 100001,
}
const buttonStyle = {
@@ -30,10 +31,10 @@ const buttonStyle = {
const AlertTemplate = ({ message, options, style, close }) => {
return (
<div style={{ ...alertStyle, ...style }}>
{options.type === 'info' && <InfoIcon />}
{options.type === 'success' && <SuccessIcon />}
{options.type === 'error' && <ErrorIcon />}
<span style={{ flex: 2 }}>{message}</span>
{options.type === 'info' && <InfoIcon style={{color: "white"}} />}
{options.type === 'success' && <CheckIcon style={{color: "green", }}/>}
{options.type === 'error' && <ErrorOutlineIcon style={{color: "red"}} />}
<Typography style={{marginLeft: 15, flex: 2 }}>{message}</Typography>
<button onClick={close} style={buttonStyle}>
<CloseIcon />
</button>
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
import { createBrowserHistory } from 'history';
var localExport
if (typeof window !== 'undefined') {
localExport = createBrowserHistory({forceRefresh: true});
}
export default localExport
+488 -62
View File
@@ -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,12 +23,13 @@ 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";
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';
@@ -41,10 +45,19 @@ 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);
@@ -54,11 +67,13 @@ const Admin = (props) => {
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([]);
const [organizations, setOrganizations] = React.useState([]);
const [orgSyncResponse, setOrgSyncResponse] = React.useState("");
const [userSettings, setUserSettings] = React.useState({});
const [environments, setEnvironments] = React.useState([]);
const [authentication, setAuthentication] = React.useState([]);
@@ -70,6 +85,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',
@@ -157,7 +173,7 @@ const Admin = (props) => {
alert.error("Failed stopping schedule")
} else {
getAppAuthentication()
alert.success("Successfully stopped schedule!")
alert.success("Successfully deleted authentication!")
}
}),
)
@@ -237,13 +253,17 @@ const Admin = (props) => {
getOrgs()
if (disableSync) {
alert.success("Successfully disabled sync!")
setOrgSyncResponse("Successfully disabled syncronization")
} else {
alert.success("Sync successfully set up!")
alert.success("Cloud Syncronization successfully set up!")
setOrgSyncResponse("Successfully started syncronization. Cloud features you now have access to can be seen below.")
}
selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync
setSelectedOrganization(selectedOrganization)
setCloudSyncApikey("")
handleGetOrg(userdata.active_org.id)
}
})
.catch(error => {
@@ -252,6 +272,10 @@ const Admin = (props) => {
})
}
const onPasswordChange = () => {
const data = { "username": selectedUser.username, "newpassword": newPassword }
const url = globalUrl + '/api/v1/users/passwordchange';
@@ -312,6 +336,58 @@ 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 => {
if (response.status === 401) {
}
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)
setOrganizationFeatures(lists)
}
})
.catch(error => {
console.log("Error getting org: ", error)
alert.error("Error getting current organization")
});
}
const submitUser = (data) => {
console.log("INPUT: ", data)
@@ -492,6 +568,7 @@ const Admin = (props) => {
return response.json()
})
.then((responseJson) => {
console.log(responseJson)
setSchedules(responseJson)
})
.catch(error => {
@@ -518,7 +595,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")
@@ -604,9 +681,59 @@ 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,
"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) {
//setSelectedOrganization(userdata.active_org)
handleGetOrg(userdata.active_org.id)
}
const paperStyle = {
@@ -822,6 +949,8 @@ const Admin = (props) => {
</Dialog>
const GridItem = (props) => {
const [expanded, setExpanded] = React.useState(false)
const primary = props.data.primary
const secondary = props.data.secondary
const primaryIcon = props.data.icon
@@ -831,19 +960,36 @@ const Admin = (props) => {
<CloseIcon style={{color: "red"}} />
return (
<Grid item xs={6}>
<ListItem>
<ListItemAvatar>
<Avatar>
{primaryIcon}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={primary}
secondary={secondary}
/>
{secondaryIcon}
</ListItem>
<Grid item xs={4} style={{cursor: "pointer"}} onClick={() => {
setExpanded(!expanded)
}}>
<Card style={{margin: 4, backgroundColor: theme.palette.inputColor, color: "white", minHeight: expanded ? 200 : "inherit", maxHeight: expanded ? 200 : "inherit",}}>
<ListItem>
<ListItemAvatar>
<Avatar>
{primaryIcon}
</Avatar>
</ListItemAvatar>
<ListItemText
style={{textTransform: "capitalize"}}
primary={primary}
/>
{secondaryIcon}
</ListItem>
{expanded ?
<div style={{padding: 15}}>
<Typography>
Usage: {props.data.limit === 0 ? "Infinite" : <span>{props.data.usage} / {props.data.limit}</span>}
</Typography>
<Typography>
Data sharing: {props.data.data_collection}
</Typography>
<Typography>
Description: {secondary}
</Typography>
</div>
: null}
</Card>
</Grid>
)
}
@@ -853,15 +999,21 @@ const Admin = (props) => {
{
"primary": "Workflows",
"secondary": "",
"active": false,
"active": true,
"icon": <PolymerIcon style={{color: itemColor}}/>,
},
{
"primary": "Apps",
"secondary": "",
"active": false,
"active": true,
"icon": <AppsIcon style={{color: itemColor}}/>,
},
{
"primary": "Organization",
"secondary": "",
"active": true,
"icon": <BusinessIcon style={{color: itemColor}}/>,
},
]
const cloudSyncModal =
@@ -881,7 +1033,7 @@ const Admin = (props) => {
Enable cloud features
</span></DialogTitle>
<DialogContent>
What does <a href="https://shuffler.io/docs/hybrid#cloud_sync" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>cloud sync</a> do?
What does <a href="https://shuffler.io/docs/organizations#cloud_sync" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>cloud sync</a> do?
<div style={{display: "flex", marginBottom: 20, }}>
<TextField
color="primary"
@@ -905,7 +1057,7 @@ const Admin = (props) => {
setCloudSyncApikey(event.target.value)
}}
/>
<Button disabled={(!selectedOrganization.cloud_sync && cloudSyncApikey.length === 0) || loading} variant="contained" style={{ marginLeft: 15, height: 60, margin: "auto", borderRadius: "0px" }} onClick={() => {
<Button disabled={(!selectedOrganization.cloud_sync && cloudSyncApikey.length === 0) || loading} variant="contained" style={{ marginLeft: 15, height: 50, borderRadius: "0px" }} onClick={() => {
setLoading(true)
enableCloudSync(
cloudSyncApikey,
@@ -934,13 +1086,236 @@ const Admin = (props) => {
)
})}
</Grid>
* New triggers (userinput, hotmail realtime)<div/>
* Execute in the cloud rather than onprem<div/>
* Apps can be built in the cloud<div/>
* Easily share apps and workflows<div/>
* Access to powerful cloud search
</DialogContent>
</Dialog>
</Dialog>
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 organizationView = curTab === 0 && selectedOrganization.id !== undefined ?
<div>
<div style={{ marginTop: 20, marginBottom: 20, }}>
<h2 style={{ display: "inline", }}>Organization overview</h2>
<span style={{ marginLeft: 25 }}>
On this page you can configure individual parts of your organization. <a target="_blank" href="https://shuffler.io/docs/organizations#organization" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a>
</span>
</div>
{selectedOrganization.id === undefined ?
<div style={{height: 250}}/>
:
<div>
{selectedOrganization.name.length > 0 ?
<OrgHeader setSelectedOrganization={setSelectedOrganization} globalUrl={globalUrl} selectedOrganization={selectedOrganization}/>
: null}
<Divider style={{ marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor }} />
<Typography variant="h6" style={{marginBottom: "10px", color: "white"}}>Cloud syncronization</Typography>
What does <a href="https://shuffler.io/docs/organizations#cloud_sync" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>cloud sync</a> do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will <b>ALWAYS</b> make every option open source, but features relying on other users can't be done without a collaborative approach.
{isCloud ?
<div style={{marginTop: 15, display: "flex"}}>
<div style={{flex: 1}}>
<Typography style={{}}>
Currently syncronizing: {selectedOrganization.cloud_sync_active === true ? "True" : "False"}
</Typography>
{selectedOrganization.cloud_sync_active ?
<Typography style={{}}>
Syncronization interval: {selectedOrganization.sync_config.interval === 0 ? "60" : selectedOrganization.sync_config.interval}
</Typography>
:
null
}
<Typography style={{whiteSpace: "nowrap", marginTop: 25, marginRight: 10}}>
Your Apikey
</Typography>
<TextField
color="primary"
style={{backgroundColor: theme.palette.inputColor, }}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
disabled={true}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
defaultValue={userSettings.apikey}
/>
</div>
</div>
:
<div>
<div style={{display: "flex", marginBottom: 20, }}>
<TextField
color="primary"
style={{backgroundColor: theme.palette.inputColor, marginRight: 10, }}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
disabled={selectedOrganization.cloud_sync}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
onChange={(event) => {
setCloudSyncApikey(event.target.value)
}}
/>
<Button disabled={(!selectedOrganization.cloud_sync && cloudSyncApikey.length === 0) || loading} variant="contained" style={{marginTop: 15, height: 50, width: 150,}} onClick={() => {
setLoading(true)
enableCloudSync(
cloudSyncApikey,
selectedOrganization,
selectedOrganization.cloud_sync,
)
}} color="primary">
{selectedOrganization.cloud_sync ?
"Stop sync"
:
"Start sync"
}
</Button>
</div>
{orgSyncResponse.length > 0 ?
<Typography style={{marginTop: 5, marginBottom: 10}}>
Message from Shuffle: <b>{orgSyncResponse}</b>
</Typography>
: null
}
</div>
}
<Typography style={{marginTop: 40, marginLeft: 10, marginBottom: 5,}}>Cloud sync features</Typography>
<Grid container style={{width: "100%", marginBottom: 15, }}>
{Object.keys(selectedOrganization.sync_features).map(function(key, index) {
if (key === "schedule") {
return null
}
const item = selectedOrganization.sync_features[key]
const newkey = key.replace("_", " ")
const griditem = {
"primary": newkey,
"secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description,
"limit": item.limit,
"usage": 0,
"data_collection": "None",
"active": item.active,
"icon": <PolymerIcon style={{color: itemColor}}/>,
}
return (
<Zoom key={index} >
<GridItem data={griditem} />
</Zoom>
)
})}
</Grid>
<Divider style={{ marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor }} />
{isCloud && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ?
<div style={{marginTop: 30, marginBottom: 20}}>
<Typography style={{marginTop: 40, marginLeft: 10, marginBottom: 5,}}>
Your subscription{selectedOrganization.subscriptions.length > 1 ? "s" : ""}
</Typography>
<Grid container spacing={3} style={{marginTop: 15}}>
{selectedOrganization.subscriptions.reverse().map((sub, index) => {
return (
<Grid item key={index} xs={4}>
<Card elevation={6} style={{backgroundColor: theme.palette.inputColor, color: "white", padding: 25, textAlign: "left",}}>
<b>Type</b>: {sub.level}<div/>
<b>Recurrence</b>: {sub.recurrence}<div/>
{sub.active ?
<div>
<b>Started</b>: {new Date(sub.startdate*1000).toISOString()}<div/>
<Button variant="outlined" color="primary" style={{marginTop: 15}} onClick={() => {
cancelSubscriptions(sub.reference)
}}>
Cancel subscription
</Button>
</div>
:
<div>
<b>Cancelled</b>: {new Date(sub.cancellationdate*1000).toISOString()}<div/>
<Typography color="textSecondary">
<b>Status</b>: Deactivated
</Typography>
</div>
}
</Card>
</Grid>
)
})}
</Grid>
<Divider style={{ marginTop: 20, backgroundColor: theme.palette.inputColor }} />
</div>
: null
}
</div>
}
<div style={{backgroundColor: "#1f2023", paddingTop: 25,}}>
<HandlePayment stripeKey={props.stripeKey} userdata={userdata} globalUrl={globalUrl} {...props} />
</div>
</div>
: null
const modalView =
<Dialog
@@ -956,10 +1331,10 @@ const Admin = (props) => {
}}
>
<DialogTitle><span style={{ color: "white" }}>
{curTab === 0 ? "Add user" : "Add environment"}
{curTab === 1 ? "Add user" : "Add environment"}
</span></DialogTitle>
<DialogContent>
{curTab === 0 ?
{curTab === 1 ?
<div>
Username
<TextField
@@ -1004,7 +1379,7 @@ const Admin = (props) => {
onChange={(event) => changeModalData("Password", event.target.value)}
/>
</div>
: curTab === 2 ?
: curTab === 3 ?
<div>
Environment Name
<TextField
@@ -1035,9 +1410,9 @@ const Admin = (props) => {
Cancel
</Button>
<Button variant="contained" style={{ borderRadius: "0px" }} onClick={() => {
if (curTab === 0) {
if (curTab === 1) {
submitUser(modalUser)
} else if (curTab === 2) {
} else if (curTab === 3) {
submitEnvironment(modalUser)
}
}} color="primary">
@@ -1046,11 +1421,11 @@ const Admin = (props) => {
</DialogActions>
</Dialog>
const usersView = curTab === 0 ?
const usersView = curTab === 1 ?
<div>
<div style={{ marginTop: 20, marginBottom: 20, }}>
<h2 style={{ display: "inline", }}>User management</h2>
<span style={{ marginLeft: 25 }}>Add, edit, block or change passwords</span>
<span style={{ marginLeft: 25 }}>Add, edit, block or change passwords. <a target="_blank" href="https://shuffler.io/docs/organizations#user_management" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a></span>
</div>
<div />
<Button
@@ -1068,10 +1443,12 @@ const Admin = (props) => {
primary="Username"
style={{ minWidth: 200, maxWidth: 200 }}
/>
<ListItemText
primary="API key"
style={{ minWidth: 350, maxWidth: 350, overflow: "hidden" }}
/>
<ListItemText
primary="Role"
style={{ minWidth: 150, maxWidth: 150 }}
@@ -1092,10 +1469,12 @@ const Admin = (props) => {
primary={data.username}
style={{ minWidth: 200, maxWidth: 200 }}
/>
<ListItemText
primary={data.apikey === undefined || data.apikey.length === 0 ? "" : data.apikey}
style={{ maxWidth: 350, minWidth: 350, }}
/>
<ListItemText
primary=
{<Select
@@ -1137,7 +1516,15 @@ const Admin = (props) => {
>
Edit user
</Button>
</ListItemText>
<Button
style={{}}
variant="outlined"
color="primary"
onClick={() => generateApikey(data.id)}
>
Get new API key
</Button>
</ListItemText>
</ListItem>
)
})}
@@ -1145,37 +1532,53 @@ const Admin = (props) => {
</div>
: null
const schedulesView = curTab === 3 ?
const schedulesView = curTab === 4 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>Schedules</h2>
<span style={{marginLeft: 25}}>Schedules used in Workflows. Makes locating and control easier.</span>
<span style={{marginLeft: 25}}>Schedules used in Workflows. Makes locating and control easier. <a target="_blank" href="https://shuffler.io/docs/organizations#schedules" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a></span>
</div>
<Divider style={{marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor}}/>
<List>
<ListItem>
<ListItemText
primary="Interval (seconds)"
style={{maxWidth: 200}}
primary="Interval"
style={{maxWidth: 200, minWidth: 200}}
/>
<ListItemText
primary="Environment"
style={{maxWidth: 150, minWidth: 150}}
/>
<ListItemText
primary="Workflow"
style={{maxWidth: 315, minWidth: 315}}
/>
<ListItemText
primary="Argument"
style={{maxWidth: 400, overflow: "hidden"}}
style={{minWidth: 300, maxWidth: 300, overflow: "hidden"}}
/>
<ListItemText
primary="Actions"
/>
</ListItem>
{schedules === undefined || schedules === null ? null : schedules.map(schedule => {
{schedules === undefined || schedules === null ? null : schedules.map((schedule, index) => {
return (
<ListItem>
<ListItem key={index}>
<ListItemText
style={{maxWidth: 200}}
primary={schedule.seconds}
style={{maxWidth: 200, minWidth: 200}}
primary={schedule.environment === "cloud" ? schedule.frequency : <span>{schedule.seconds} seconds</span>}
/>
<ListItemText
style={{maxWidth: 150, minWidth: 150}}
primary={schedule.environment}
/>
<ListItemText
style={{maxWidth: 315, minWidth: 315}}
primary={<a style={{textDecoration: "none", color: "#f85a3e"}} href={`/workflows/${schedule.workflow_id}`} target="_blank">{schedule.workflow_id}</a>}
/>
<ListItemText
primary={schedule.argument}
style={{maxWidth: 400, overflow: "hidden"}}
style={{minWidth: 300, maxWidth: 300, overflow: "hidden"}}
/>
<ListItemText>
<Button
@@ -1184,7 +1587,7 @@ const Admin = (props) => {
color="primary"
onClick={() => deleteSchedule(schedule)}
>
Delete
Stop schedule
</Button>
</ListItemText>
</ListItem>
@@ -1194,7 +1597,7 @@ const Admin = (props) => {
</div>
: null
const appCategoryView = curTab === 6 ?
const appCategoryView = curTab === 7 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>Categories</h2>
@@ -1270,11 +1673,12 @@ const Admin = (props) => {
</div>
: null
const authenticationView = curTab === 1 ?
const authenticationView = curTab === 2 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>App Authentication</h2>
<span style={{marginLeft: 25}}>Control the authentication options for individual apps. <b>Actions can be destructive!</b></span>
.&nbsp;<a target="_blank" href="https://shuffler.io/docs/organizations#app_authentication" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a>
</div>
<Divider style={{marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor}}/>
<List>
@@ -1307,9 +1711,9 @@ const Admin = (props) => {
primary="Actions"
/>
</ListItem>
{authentication === undefined ? null : authentication.map(data => {
{authentication === undefined ? null : authentication.map((data, index) => {
return (
<ListItem>
<ListItem key={index}>
<ListItemText
primary=<img alt="" src={data.app.large_image} style={{maxWidth: 50,}} />
style={{minWidth: 150, maxWidth: 150}}
@@ -1355,11 +1759,11 @@ const Admin = (props) => {
</div>
: null
const environmentView = curTab === 2 ?
const environmentView = curTab === 3 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>Environments</h2>
<span style={{marginLeft: 25}}>Decides what Orborus environment to execute an action in a workflow in.</span>
<span style={{marginLeft: 25}}>Decides what Orborus environment to execute an action in a workflow in.<a target="_blank" href="https://shuffler.io/docs/organizations#environments" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a></span>
</div>
<Button
style={{}}
@@ -1423,7 +1827,7 @@ const Admin = (props) => {
style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}}
/>
<ListItemText
primary={"TBD"}
primary={environment.Type === "cloud" ? "N/A" : "TBD"}
style={{minWidth: 200, maxWidth: 200, overflow: "hidden"}}
/>
<ListItemText
@@ -1443,7 +1847,7 @@ const Admin = (props) => {
<ListItemText
style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}}
>
<Button variant="outlined" style={{borderRadius: "0px"}} onClick={() => deleteEnvironment(environment.Name)} color="primary">Delete</Button>
<Button disabled={environment.archived} variant="outlined" style={{borderRadius: "0px"}} onClick={() => deleteEnvironment(environment.Name)} color="primary">Archive</Button>
</ListItemText>
<ListItemText
style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}}
@@ -1456,7 +1860,7 @@ const Admin = (props) => {
</div>
: null
const organizationsTab = curTab === 5 ?
const organizationsTab = curTab === 6 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>Organizations</h2>
@@ -1537,7 +1941,7 @@ const Admin = (props) => {
</div>
: null
const hybridTab = curTab === 4 ?
const hybridTab = curTab === 5 ?
<div>
<div style={{marginTop: 20, marginBottom: 20,}}>
<h2 style={{display: "inline",}}>Hybrid</h2>
@@ -1581,12 +1985,14 @@ const Admin = (props) => {
const setConfig = (event, newValue) => {
if (newValue === 1) {
getAppAuthentication()
getUsers()
} else if (newValue === 2) {
getEnvironments()
getAppAuthentication()
} else if (newValue === 3) {
getEnvironments()
} else if (newValue === 4) {
getSchedules()
} else if (newValue === 5) {
} else if (newValue === 6) {
getOrgs()
}
@@ -1594,6 +2000,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)
}
@@ -1608,16 +2032,18 @@ const Admin = (props) => {
onChange={setConfig}
aria-label="disabled tabs example"
>
<Tab label=<span><AccessibilityNewIcon style={iconStyle} />Users</span> />
<Tab label=<span><LockIcon style={iconStyle} />App Authentication</span>/>
<Tab label=<span><EcoIcon style={iconStyle} />Environments</span>/>
<Tab label=<span><ScheduleIcon style={iconStyle} />Schedules</span> />
<Tab label=<span><BusinessIcon style={iconStyle} /> Organization</span>/>
{isCloud ? null : <Tab label=<span><AccessibilityNewIcon style={iconStyle} />Users</span> />}
{isCloud ? null : <Tab label=<span><LockIcon style={iconStyle} />App Authentication</span>/>}
{isCloud ? null : <Tab label=<span><EcoIcon style={iconStyle} />Environments</span>/>}
{isCloud ? null : <Tab label=<span><ScheduleIcon style={iconStyle} />Schedules</span> />}
{window.location.protocol == "http:" && window.location.port === "3000" ? <Tab label=<span><CloudIcon style={iconStyle} /> Hybrid</span>/> : null}
{window.location.protocol == "http:" && window.location.port === "3000" ? <Tab label=<span><BusinessIcon style={iconStyle} /> Organizations</span>/> : null}
{window.location.protocol === "http:" && window.location.port === "3000" ? <Tab label=<span><LockIcon style={iconStyle} />Categories</span>/> : null}
</Tabs>
<Divider style={{marginTop: 0, marginBottom: 10, backgroundColor: "rgb(91, 96, 100)"}} />
<div style={{padding: 15}}>
{organizationView}
{authenticationView}
{appCategoryView}
{usersView}
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -884,6 +884,7 @@ const AppCreator = (props) => {
}
})
.catch(error => {
setAppBuilding(false)
setErrorCode(error.toString())
alert.error(error.toString())
});
@@ -1568,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"
@@ -1576,7 +1577,7 @@ const AppCreator = (props) => {
multiline
rows="5"
onChange={e => setActionField("headers", e.target.value)}
helperText={<span style={{color:"white", marginBottom: "2px",}}>Headers that are part of the request</span>}
helperText={<span style={{color:"white", marginBottom: "2px",}}>Headers that are part of the request. Default: EMPTY</span>}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
+36 -31
View File
@@ -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) => {
</div>
</div>
{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) ?
<div>
{downloadButton}
{editButton}
@@ -618,7 +619,7 @@ const Apps = (props) => {
})}
</div>
: null}
{props.userdata.id === selectedApp.owner ?
{props.userdata !== undefined && props.userdata.id === selectedApp.owner ?
<div style={{marginTop: 15}}>
{/*<p><b>ID:</b> {selectedApp.id}</p>*/}
<b style={{marginRight: 15}}>Sharing:</b>
@@ -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) => {
</div>
<Divider style={{marginBottom: 10, marginTop: 10, height: "100%", width: 1, backgroundColor: dividerColor}}/>
<div style={{flex: 1, marginLeft: 10, marginRight: 10}}>
<div style={{display: "flex"}}>
<div style={{display: "flex", minHeight: 84.81}}>
<div style={{flex: 1}}>
<h2>Your apps ({apps.length+searchableApps.length})</h2>
</div>
<Tooltip title={"Reload apps locally"} style={{marginTop: "28px", width: "100%"}} aria-label={"Upload"}>
<Button
variant="outlined"
component="label"
color="primary"
style={{margin: 5, maxHeight: 50, marginTop: 10}}
onClick={() => {
hotloadApps()
}}
>
<CachedIcon />
</Button>
</Tooltip>
<Tooltip title={"Download from Github"} style={{marginTop: "28px", width: "100%"}} aria-label={"Upload"}>
<Button
variant="outlined"
component="label"
color="primary"
style={{margin: 5, maxHeight: 50, marginTop: 10}}
onClick={() => {
setOpenApi(baseRepository)
setLoadAppsModalOpen(true)
}}
>
<CloudDownloadIcon />
</Button>
</Tooltip>
{isCloud ? null :
<span>
<Tooltip title={"Reload apps locally"} style={{marginTop: "28px", width: "100%"}} aria-label={"Upload"}>
<Button
variant="outlined"
component="label"
color="primary"
style={{margin: 5, maxHeight: 50, marginTop: 10}}
onClick={() => {
hotloadApps()
}}
>
<CachedIcon />
</Button>
</Tooltip>
<Tooltip title={"Download from Github"} style={{marginTop: "28px", width: "100%"}} aria-label={"Upload"}>
<Button
variant="outlined"
component="label"
color="primary"
style={{margin: 5, maxHeight: 50, marginTop: 10}}
onClick={() => {
setOpenApi(baseRepository)
setLoadAppsModalOpen(true)
}}
>
<CloudDownloadIcon />
</Button>
</Tooltip>
</span>
}
</div>
<TextField
style={{backgroundColor: inputColor}}
+5 -3
View File
@@ -22,8 +22,9 @@ const Body = {
const dividerColor = "rgb(225, 228, 232)"
const SideBar = {
maxWidth: "250px",
maxWidth: 250,
flex: "1",
position: "fixed",
}
const hrefStyle = {
@@ -165,6 +166,7 @@ const Docs = (props) => {
maxWidth: 750,
overflow: "hidden",
paddingBottom: 200,
marginLeft: 250,
}
function OuterLink(props) {
@@ -212,11 +214,11 @@ const Docs = (props) => {
<div style={Body}>
<div style={SideBar}>
<ul style={{listStyle: "none", paddingLeft: "0"}}>
{list.map(item => {
{list.map((item, index) => {
const path = "/docs/"+item
const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ")
return (
<li style={{marginTop: "10px"}}>
<li key={index} style={{marginTop: "10px"}}>
<Link style={hrefStyle} to={path} onClick={() => {fetchDocs(item)}}>
<h2>{newname}</h2>
</Link>
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
const HandlePayment = () => {
return (
null
)
}
export default HandlePayment
+2 -1
View File
@@ -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
+4 -4
View File
@@ -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
+33 -8
View File
@@ -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,18 +135,36 @@ 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)
return
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,
)
if err != nil {
log.Printf("[ERROR] Container create error(2): %s", err)
return
}
} else {
log.Printf("[ERROR] Container create error: %s", err)
return
}
}
err = dockercli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
@@ -208,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),
@@ -301,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")
+4 -4
View File
@@ -1,12 +1,12 @@
NAME=worker
VERSION=0.6.0
NAME=shuffle-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..
#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
+120 -11
View File
@@ -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,14 +791,51 @@ 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)
// 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{}
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
}
}
// Not really sure how this edgecase happens.
// FIXME
@@ -816,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
@@ -911,7 +964,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,13 +1026,13 @@ 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)
}
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 {
@@ -1075,16 +1128,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 +1283,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(