@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+433
-39
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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" ?
|
||||
|
||||
@@ -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
@@ -0,0 +1,8 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
var localExport
|
||||
if (typeof window !== 'undefined') {
|
||||
localExport = createBrowserHistory({forceRefresh: true});
|
||||
}
|
||||
|
||||
export default localExport
|
||||
+488
-62
@@ -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>
|
||||
. <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
@@ -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
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const HandlePayment = () => {
|
||||
return (
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export default HandlePayment
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user