Merge pull request #1676 from Shuffle/nightly

2.0.2
This commit is contained in:
Frikky
2025-04-03 23:14:49 +02:00
committed by GitHub
45 changed files with 2673 additions and 1858 deletions
@@ -0,0 +1,82 @@
name: nightly-dockerbuild
on:
workflow_dispatch:
push:
branches:
- nightly
paths:
- "**"
- "!.github/**"
- "!**.md"
- "!docker-compose.yml"
jobs:
main:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- app: frontend
path: frontend
version: nightly
experimental: true
- app: backend
path: backend
version: nightly
experimental: true
- app: orborus
path: functions/onprem/orborus
version: nightly
experimental: true
- app: worker
path: functions/onprem/worker
version: nightly
experimental: true
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: "amd64,arm64,arm"
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Ghcr
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Ghcr Build and push
id: docker_build
uses: docker/build-push-action@v4
env:
BUILDX_NO_DEFAULT_LOAD: true
with:
logout: false
context: ${{ matrix.path }}/
file: ${{ matrix.path }}/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ matrix.version }}
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ matrix.version }}
frikky/shuffle-${{ matrix.app }}:${{ matrix.version }}
frikky/shuffle:${{ matrix.app }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+12 -7
View File
@@ -33,16 +33,21 @@ jobs:
sudo apt-get install helm -y --no-install-recommends
- name: Set versions
id: set_versions
run: |
if [[ ${{ github.event_name }} == 'release' ]]; then
CHART_VERSION="${{ github.event.release.tag_name }}"
APP_VERSION="${{ github.event.release.tag_name }}"
TAG_NAME="${{ github.event.release.tag_name }}"
# Remove the v prefix
VERSION=${TAG_NAME#v}
APP_VERSION="${VERSION}"
CHART_VERSION="${VERSION}"
else
CHART_VERSION="0.0.0-nightly-untagged-latest"
APP_VERSION="nightly"
CHART_VERSION="0.0.0-nightly-untagged-latest"
fi
echo "APP_VERSION set to ${APP_VERSION}"
echo "CHART_VERSION set to ${CHART_VERSION}. Validating..."
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
@@ -55,8 +60,8 @@ jobs:
exit 1;
fi
echo "CHART_VERSION=${CHART_VERSION}" >> $GITHUB_OUTPUT
echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_OUTPUT
echo "CHART_VERSION=${CHART_VERSION}" >> "$GITHUB_ENV"
echo "APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV"
- name: Update helm dependencies
run: helm dependency update ./functions/kubernetes/charts/shuffle
@@ -68,4 +73,4 @@ jobs:
run: helm registry login ghcr.io --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }}
- name: Push helm chart
run: helm push ./functions/kubernetes/charts/shuffle-*.tgz oci://ghcr.io/shuffle/shuffle/charts
run: helm push ./functions/kubernetes/charts/shuffle-*.tgz oci://ghcr.io/shuffle/charts
+56 -20
View File
@@ -604,11 +604,27 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
// return
//}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`))
return
var err error
body := []byte{}
//log.Printf("IMAGE REQUEST BODY: %#v", request.Body)
if request.Body == nil || request.Body == http.NoBody {
// Check for the image query, otherwise we skip everything
imageQuery := request.URL.Query().Get("image")
if len(imageQuery) == 0 {
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "No image query found"}`))
return
}
body = []byte(fmt.Sprintf(`{"name": "%s"}`, imageQuery))
} else {
body, err = ioutil.ReadAll(request.Body)
if err != nil {
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`))
return
}
}
// This has to be done in a weird way because Datastore doesn't
@@ -630,28 +646,48 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
return
}
img := image.Summary{}
img2 := image.Summary{}
tagFound := ""
tagFound2 := ""
// Old way of doing it
//alternativeNameSplit := strings.Split(version.Name, "/")
//alternativeName := version.Name
//if len(alternativeNameSplit) == 3 {
// alternativeName = strings.Join(alternativeNameSplit[1:3], "/")
//}
appname, baseAppname, appnameSplit2, err := shuffle.GetAppNameSplit(version)
if err != nil {
log.Printf("[ERROR] Failed getting appname split: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't get the right docker image name"}`)))
return
}
if len(version.Name) == 0 {
log.Printf("[ERROR] No image name provided for download: %s", version.Name)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "No image name"}`)))
return
}
log.Printf("[INFO] Trying to download image: '%s'. Appname: '%s'. BaseAppname: '%s', Split2: %s", version.Name, appname, baseAppname, appnameSplit2)
alternativeName := appname
ctx := context.Background()
images, err := dockercli.ImageList(ctx, image.ListOptions{
All: true,
})
img := image.Summary{}
tagFound := ""
img2 := image.Summary{}
tagFound2 := ""
alternativeNameSplit := strings.Split(version.Name, "/")
alternativeName := version.Name
if len(alternativeNameSplit) == 3 {
alternativeName = strings.Join(alternativeNameSplit[1:3], "/")
}
log.Printf("[INFO] Trying to download image: %s. Alt: %s", version.Name, alternativeName)
for _, image := range images {
for _, tag := range image.RepoTags {
//log.Printf("[DEBUG] Tag: %s", tag)
if strings.Contains(tag, "<none>") {
continue
}
if strings.ToLower(tag) == strings.ToLower(version.Name) {
img = image
tagFound = tag
+1 -1
View File
@@ -20,7 +20,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.18
github.com/shuffle/shuffle-shared v0.8.35
golang.org/x/crypto v0.36.0
google.golang.org/api v0.176.1
google.golang.org/grpc v1.68.1
+2 -2
View File
@@ -333,8 +333,8 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fc
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.18 h1:7f7cV+P2pr/g44i+AI8P0UheEe8oG8O2V8loBfe9YSw=
github.com/shuffle/shuffle-shared v0.8.18/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/shuffle/shuffle-shared v0.8.35 h1:3awc0TrsLLZiQeWD2XGIkTnFbczAG0cMfy1+cB/P7zg=
github.com/shuffle/shuffle-shared v0.8.35/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+18 -5
View File
@@ -3543,7 +3543,12 @@ func handleCloudJob(job shuffle.CloudSyncJob) error {
return err
}
redirectDomain := "localhost:5001"
backendPort := os.Getenv("BACKEND_PORT")
if backendPort == "" {
backendPort = "5001"
}
redirectDomain := fmt.Sprintf("localhost:%s", backendPort)
redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain)
outlookClient, _, err := shuffle.GetOutlookClient(ctx, "", hook.OauthToken, redirectUrl)
if err != nil {
@@ -4197,20 +4202,26 @@ func runInitEs(ctx context.Context) {
cleanupJob := func() func() {
return func() {
log.Printf("[INFO] Running schedule for cleaning up or re-running unfinished workflows in %d environments.", len(environments))
//log.Printf("[INFO] Running schedule for cleaning up or re-running unfinished workflows in %d environments.", len(environments))
backendPort := os.Getenv("BACKEND_PORT")
if backendPort == "" {
backendPort = "5001"
}
for _, environment := range environments {
// Allowed without PROXY management as it's localhost
// client := shuffle.GetExternalClient(syncUrl)
httpClient := &http.Client{}
url := fmt.Sprintf("http://localhost:5001/api/v1/environments/%s/stop", environment)
url := fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/stop", backendPort, environment)
req, err := http.NewRequest(
"GET",
url,
nil,
)
// FIXME: This will stop working of the user rotates their key lol
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, parsedApikey))
if err != nil {
log.Printf("[ERROR] Failed CREATING environment request for %s: %s", environment, err)
@@ -4231,7 +4242,7 @@ func runInitEs(ctx context.Context) {
}
log.Printf("[DEBUG] Successfully ran workflow cleanup request for %s. Body: %s", environment, string(respBody))
url = fmt.Sprintf("http://localhost:5001/api/v1/environments/%s/rerun", environment)
url = fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/rerun", backendPort, environment)
req, err = http.NewRequest(
"GET",
url,
@@ -4256,7 +4267,8 @@ func runInitEs(ctx context.Context) {
log.Printf("[ERROR] Failed setting respbody %s", err)
continue
}
log.Printf("[DEBUG] Successfully ran workflow RERUN request for %s. Body: %s", environment, string(respBody))
//log.Printf("[DEBUG] Ran workflow RERUN request for %s with the response. Body: %s", environment, string(respBody))
}
}
}
@@ -5373,6 +5385,7 @@ func main() {
if innerPort == "" {
log.Printf("[DEBUG] Running on %s:5001", hostname)
log.Fatal(http.ListenAndServe(":5001", nil))
os.Setenv("BACKEND_PORT", "5001")
} else {
log.Printf("[DEBUG] Running on %s:%s", hostname, innerPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", innerPort), nil))
+16 -517
View File
@@ -22,9 +22,6 @@ import (
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/api/types/image"
//gyaml "github.com/ghodss/yaml"
"github.com/h2non/filetype"
uuid "github.com/satori/go.uuid"
@@ -36,14 +33,6 @@ import (
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-git/go-git/v5/plumbing"
http2 "github.com/go-git/go-git/v5/plumbing/transport/http"
//http2 "gopkg.in/src-d/go-git.v5/plumbing/transport/http"
//http2 "github.com/go-git/go-git/plumbing/transport/http"
//"github.com/gorilla/websocket"
//"google.golang.org/appengine"
//"google.golang.org/appengine/memcache"
//"cloud.google.com/go/firestore"
// "google.golang.org/api/option"
gyaml "github.com/ghodss/yaml"
)
@@ -208,7 +197,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque
}
// remove items from DB
parsedId := fmt.Sprintf("workflowqueue-%s", id)
parsedId := strings.ReplaceAll(fmt.Sprintf("workflowqueue-%s", id), " ", "-")
ids := []string{}
for _, execution := range removeExecutionRequests.Data {
ids = append(ids, execution.ExecutionId)
@@ -985,7 +974,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
if err != nil {
log.Printf("[ERROR] Failed to list child workflows: %s", err)
} else {
log.Printf("\n\n[DEBUG] Found %d child workflows for workflow %s\n\n", len(childWorkflows), workflow.ID)
//log.Printf("\n\n[DEBUG] Found %d child workflows for workflow %s\n\n", len(childWorkflows), workflow.ID)
// Find cookies and append them to request.Header to replicate current request as closely as possible
for _, childWorkflow := range childWorkflows {
@@ -1141,507 +1130,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
return shuffle.WorkflowExecution{}, "Failed unmarshal during execution", err
}
/*
makeNew := true
start, startok := request.URL.Query()["start"]
if request.Method == "POST" {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[ERROR] Failed request POST read: %s", err)
return shuffle.WorkflowExecution{}, "Failed getting body", err
}
// This one doesn't really matter.
log.Printf("[INFO] Running POST execution with body of length %d for workflow %s", len(string(body)), workflowExecution.Workflow.ID)
if len(body) >= 4 {
if body[0] == 34 && body[len(body)-1] == 34 {
body = body[1 : len(body)-1]
}
if body[0] == 34 && body[len(body)-1] == 34 {
body = body[1 : len(body)-1]
}
}
sourceAuth, sourceAuthOk := request.URL.Query()["source_auth"]
if sourceAuthOk {
//log.Printf("\n\n\nSETTING SOURCE WORKFLOW AUTH TO %s!!!\n\n\n", sourceAuth[0])
workflowExecution.ExecutionSourceAuth = sourceAuth[0]
} else {
//log.Printf("Did NOT get source workflow")
}
sourceNode, sourceNodeOk := request.URL.Query()["source_node"]
if sourceNodeOk {
//log.Printf("\n\n\nSETTING SOURCE WORKFLOW NODE TO %s!!!\n\n\n", sourceNode[0])
workflowExecution.ExecutionSourceNode = sourceNode[0]
} else {
//log.Printf("Did NOT get source workflow")
}
//workflowExecution.ExecutionSource = "default"
sourceWorkflow, sourceWorkflowOk := request.URL.Query()["source_workflow"]
if sourceWorkflowOk {
//log.Printf("Got source workflow %s", sourceWorkflow)
workflowExecution.ExecutionSource = sourceWorkflow[0]
} else {
//log.Printf("Did NOT get source workflow")
}
sourceExecution, sourceExecutionOk := request.URL.Query()["source_execution"]
if sourceExecutionOk {
//log.Printf("[INFO] Got source execution%s", sourceExecution)
workflowExecution.ExecutionParent = sourceExecution[0]
} else {
//log.Printf("Did NOT get source execution")
}
if len(string(body)) < 50 {
//log.Println(body)
// String in string
//log.Println(body)
//if string(body)[0] == "\"" && string(body)[string(body)
log.Printf("[DEBUG] Body: %s", string(body))
}
var execution shuffle.ExecutionRequest
err = json.Unmarshal(body, &execution)
if err != nil {
log.Printf("[WARNING] Failed execution POST unmarshalling for execution %s - continuing anyway: %s", execution.ExecutionId, err)
//return shuffle.WorkflowExecution{}, "", err
}
if execution.Start == "" && len(body) > 0 {
execution.ExecutionArgument = string(body)
}
// FIXME - this should have "execution_argument" from executeWorkflow frontend
//log.Printf("EXEC: %#v", execution)
if len(execution.ExecutionArgument) > 0 {
workflowExecution.ExecutionArgument = execution.ExecutionArgument
}
if len(execution.ExecutionSource) > 0 {
workflowExecution.ExecutionSource = execution.ExecutionSource
}
//log.Printf("Execution data: %#v", execution)
if len(execution.Start) == 36 && len(workflow.Actions) > 0 {
log.Printf("[INFO] Should start execution on node %s", execution.Start)
workflowExecution.Start = execution.Start
found := false
for _, action := range workflow.Actions {
if action.ID == execution.Start {
found = true
break
}
}
if !found {
log.Printf("[ERROR] Action %s was NOT found! Exiting execution.", execution.Start)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", workflow.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", workflow.Start))
}
} else if len(execution.Start) > 0 {
//log.Printf("[INFO] !")
//log.Printf("[ERROR] START ACTION %s IS WRONG ID LENGTH %d!", execution.Start, len(execution.Start))
//return shuffle.WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", execution.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", execution.Start))
}
if len(execution.ExecutionId) == 36 {
workflowExecution.ExecutionId = execution.ExecutionId
} else {
sessionToken := uuid.NewV4()
workflowExecution.ExecutionId = sessionToken.String()
}
} else {
// Check for parameters of start and ExecutionId
// This is mostly used for user input trigger
answer, answerok := request.URL.Query()["answer"]
referenceId, referenceok := request.URL.Query()["reference_execution"]
if answerok && referenceok && len(answer) > 0 && len(referenceId) > 0 {
// If answer is false, reference execution with result
log.Printf("[INFO] Answer is OK AND reference is OK!")
if answer[0] == "false" {
log.Printf("Should update reference and return, no need for further execution!")
// Get the reference execution
oldExecution, err := shuffle.GetWorkflowExecution(ctx, referenceId[0])
if err != nil {
log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err
}
if oldExecution.Workflow.ID != id {
log.Println("Wrong workflowid!")
return shuffle.WorkflowExecution{}, fmt.Sprintf("Bad ID %s", referenceId), errors.New("Bad ID")
}
newResults := []shuffle.ActionResult{}
//log.Printf("%#v", oldExecution.Results)
for _, result := range oldExecution.Results {
log.Printf("%s - %s", result.Action.ID, start[0])
if result.Action.ID == start[0] {
note, noteok := request.URL.Query()["note"]
if noteok && len(note) > 0 {
result.Result = fmt.Sprintf("User note: %s", note[0])
} else {
result.Result = fmt.Sprintf("User clicked %s", answer[0])
}
// Stopping the whole thing
result.CompletedAt = int64(time.Now().Unix())
result.Status = "ABORTED"
oldExecution.Status = result.Status
oldExecution.Result = result.Result
oldExecution.LastNode = result.Action.ID
}
newResults = append(newResults, result)
}
oldExecution.Results = newResults
err = shuffle.SetWorkflowExecution(ctx, *oldExecution, true)
if err != nil {
log.Printf("Error saving workflow execution actionresult setting: %s", err)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed setting workflowexecution actionresult in execution: %s", err), err
}
return shuffle.WorkflowExecution{}, "", nil
}
}
if referenceok {
log.Printf("Handling an old execution continuation!")
// Will use the old name, but still continue with NEW ID
oldExecution, err := shuffle.GetWorkflowExecution(ctx, referenceId[0])
if err != nil {
log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err
}
workflowExecution = *oldExecution
}
if len(workflowExecution.ExecutionId) == 0 {
sessionToken := uuid.NewV4()
workflowExecution.ExecutionId = sessionToken.String()
} else {
log.Printf("Using the same executionId as before: %s", workflowExecution.ExecutionId)
makeNew = false
}
// Don't override workflow defaults
}
if startok {
//log.Printf("\n\n[INFO] Setting start to %s based on query!\n\n", start[0])
//workflowExecution.Workflow.Start = start[0]
workflowExecution.Start = start[0]
}
// FIXME - regex uuid, and check if already exists?
if len(workflowExecution.ExecutionId) != 36 {
log.Printf("Invalid uuid: %s", workflowExecution.ExecutionId)
return shuffle.WorkflowExecution{}, "Invalid uuid", err
}
// FIXME - find owner of workflow
// FIXME - get the actual workflow itself and build the request
// MAYBE: Don't send the workflow within the pubsub, as this requires more data to be sent
// Check if a worker already exists for company, else run one with:
// locations, project IDs and subscription names
// When app is executed:
// Should update with status execution (somewhere), which will trigger the next node
// IF action.type == internal, we need the internal watcher to be running and executing
// This essentially means the WORKER has to be the responsible party for new actions in the INTERNAL landscape
// Results are ALWAYS posted back to cloud@execution_id?
if makeNew {
workflowExecution.Type = "workflow"
//workflowExecution.Stream = "tmp"
//workflowExecution.WorkflowQueue = "tmp"
//workflowExecution.SubscriptionNameNodestream = "testcompany-nodestream"
//workflowExecution.Locations = []string{"europe-west2"}
workflowExecution.ProjectId = gceProject
workflowExecution.WorkflowId = workflow.ID
workflowExecution.StartedAt = int64(time.Now().Unix())
workflowExecution.CompletedAt = 0
workflowExecution.Authorization = uuid.NewV4().String()
// Status for the entire workflow.
workflowExecution.Status = "EXECUTING"
}
if len(workflowExecution.ExecutionSource) == 0 {
log.Printf("[INFO] No execution source (trigger) specified. Setting to default")
workflowExecution.ExecutionSource = "default"
} else {
log.Printf("[INFO] Execution source is %s for execution ID %s in workflow %s", workflowExecution.ExecutionSource, workflowExecution.ExecutionId, workflowExecution.Workflow.ID)
}
workflowExecution.ExecutionVariables = workflow.ExecutionVariables
if len(workflowExecution.Start) == 0 && len(workflowExecution.Workflow.Start) > 0 {
workflowExecution.Start = workflowExecution.Workflow.Start
}
startnodeFound := false
newStartnode := ""
for _, item := range workflowExecution.Workflow.Actions {
if item.ID == workflowExecution.Start {
startnodeFound = true
}
if item.IsStartNode {
newStartnode = item.ID
}
}
if !startnodeFound {
log.Printf("[INFO] Couldn't find startnode %s. Remapping to %#v", workflowExecution.Start, newStartnode)
if len(newStartnode) > 0 {
workflowExecution.Start = newStartnode
} else {
return shuffle.WorkflowExecution{}, fmt.Sprintf("Startnode couldn't be found"), errors.New("Startnode isn't defined in this workflow..")
}
}
childNodes := shuffle.FindChildNodes(workflowExecution.Workflow, workflowExecution.Start, []string{}, []string{})
startFound := false
newActions := []shuffle.Action{}
defaultResults := []shuffle.ActionResult{}
for _, action := range workflowExecution.Workflow.Actions {
//action.LargeImage = ""
if action.ID == workflowExecution.Start {
startFound = true
}
//log.Println(action.Environment)
if action.Environment == "" {
return shuffle.WorkflowExecution{}, fmt.Sprintf("Environment is not defined for %s", action.Name), errors.New("Environment not defined!")
}
action.LargeImage = ""
if len(action.Label) == 0 {
action.Label = action.ID
}
//log.Printf("LABEL: %s", action.Label)
newActions = append(newActions, action)
// 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 len(workflowExecution.Results) > 0 {
defaultResults = []shuffle.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 {
//log.Printf("Found %s", action.ID)
found = true
}
}
if !found {
if action.ID == workflowExecution.Start {
continue
}
//log.Printf("[WARNING] Set %s to SKIPPED as it's NOT a childnode of the startnode.", action.ID)
curaction := shuffle.Action{
AppName: action.AppName,
AppVersion: action.AppVersion,
Label: action.Label,
Name: action.Name,
ID: action.ID,
}
//action
//curaction.Parameters = []
defaultResults = append(defaultResults, shuffle.ActionResult{
Action: curaction,
ExecutionId: workflowExecution.ExecutionId,
Authorization: workflowExecution.Authorization,
Result: "Skipped because it's not under the startnode",
StartedAt: 0,
CompletedAt: 0,
Status: "SKIPPED",
})
}
}
}
removeTriggers := []string{}
for triggerIndex, trigger := range workflowExecution.Workflow.Triggers {
//log.Printf("[INFO] ID: %s vs %s", trigger.ID, workflowExecution.Start)
if trigger.ID == workflowExecution.Start {
if trigger.AppName == "User Input" {
startFound = true
break
}
}
if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" {
found := false
for _, node := range childNodes {
if node == trigger.ID {
found = true
break
}
}
if !found {
//log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID)
curaction := shuffle.Action{
AppName: "shuffle-subflow",
AppVersion: trigger.AppVersion,
Label: trigger.Label,
Name: trigger.Name,
ID: trigger.ID,
}
defaultResults = append(defaultResults, shuffle.ActionResult{
Action: curaction,
ExecutionId: workflowExecution.ExecutionId,
Authorization: workflowExecution.Authorization,
Result: "Skipped because it's not under the startnode",
StartedAt: 0,
CompletedAt: 0,
Status: "SKIPPED",
})
} else {
// Replaces trigger with the subflow
//if trigger.AppName == "Shuffle Workflow" {
// replaceActions := false
// workflowAction := ""
// for _, param := range trigger.Parameters {
// if param.Name == "argument" && !strings.Contains(param.Value, ".#") {
// replaceActions = true
// }
// if param.Name == "startnode" {
// workflowAction = param.Value
// }
// }
// if replaceActions {
// replacementNodes, newBranches, lastnode := shuffle.GetReplacementNodes(ctx, workflowExecution, trigger, trigger.Label)
// log.Printf("REPLACEMENTS: %d, %d", len(replacementNodes), len(newBranches))
// if len(replacementNodes) > 0 {
// for _, action := range replacementNodes {
// found := false
// for subActionIndex, subaction := range newActions {
// if subaction.ID == action.ID {
// found = true
// //newActions[subActionIndex].Name = action.Name
// newActions[subActionIndex].Label = action.Label
// break
// }
// }
// if !found {
// action.SubAction = true
// newActions = append(newActions, action)
// }
// // Check if it's already set to have a value
// for resultIndex, result := range defaultResults {
// if result.Action.ID == action.ID {
// defaultResults = append(defaultResults[:resultIndex], defaultResults[resultIndex+1:]...)
// break
// }
// }
// }
// for _, branch := range newBranches {
// workflowExecution.Workflow.Branches = append(workflowExecution.Workflow.Branches, branch)
// }
// // Append branches:
// // parent -> new inner node (FIRST one)
// for branchIndex, branch := range workflowExecution.Workflow.Branches {
// if branch.DestinationID == trigger.ID {
// log.Printf("REPLACE DESTINATION WITH %s!!", workflowAction)
// workflowExecution.Workflow.Branches[branchIndex].DestinationID = workflowAction
// }
// if branch.SourceID == trigger.ID {
// log.Printf("REPLACE SOURCE WITH LASTNODE %s!!", lastnode)
// workflowExecution.Workflow.Branches[branchIndex].SourceID = lastnode
// }
// }
// // Remove the trigger
// removeTriggers = append(removeTriggers, workflowExecution.Workflow.Triggers[triggerIndex].ID)
// }
// log.Printf("NEW ACTION LENGTH %d, RESULT: %d, Triggers: %d, BRANCHES: %d", len(newActions), len(defaultResults), len(workflowExecution.Workflow.Triggers), len(workflowExecution.Workflow.Branches))
// }
//}
_ = triggerIndex
}
}
}
//newTriggers := []shuffle.Trigger{}
//for _, trigger := range workflowExecution.Workflow.Triggers {
// found := false
// for _, triggerId := range removeTriggers {
// if trigger.ID == triggerId {
// found = true
// break
// }
// }
// if found {
// log.Printf("[WARNING] Removed trigger %s during execution", trigger.ID)
// continue
// }
// newTriggers = append(newTriggers, trigger)
//}
//workflowExecution.Workflow.Triggers = newTriggers
if !startFound {
if len(workflowExecution.Start) == 0 && len(workflowExecution.Workflow.Start) > 0 {
workflowExecution.Start = workflow.Start
} else if len(workflowExecution.Workflow.Actions) > 0 {
workflowExecution.Start = workflowExecution.Workflow.Actions[0].ID
} else {
log.Printf("[ERROR] Startnode %s doesn't exist!!", workflowExecution.Start)
return shuffle.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))
}
}
//log.Printf("EXECUTION START: %s", workflowExecution.Start)
// Verification for execution environments
workflowExecution.Results = defaultResults
workflowExecution.Workflow.Actions = newActions
onpremExecution := true
_ = onpremExecution
environments := []string{}
if len(workflowExecution.ExecutionOrg) == 0 && len(workflow.ExecutingOrg.Id) > 0 {
workflowExecution.ExecutionOrg = workflow.ExecutingOrg.Id
}
*/
//workflowExecution, execInfo, _, workflowExecErr := shuffle.PrepareWorkflowExecution(ctx, workflow, request, int64(maxExecutionDepth))
err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
@@ -1652,7 +1140,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
environments := execInfo.Environments
var allEnvs []shuffle.Environment
if len(workflowExecution.ExecutionOrg) > 0 {
//log.Printf("[INFO] Executing ORG: %s", workflowExecution.ExecutionOrg)
allEnvironments, err := shuffle.GetEnvironments(ctx, workflowExecution.ExecutionOrg)
if err != nil {
@@ -3557,9 +3044,21 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
return
}
time.Sleep(2 * time.Second)
log.Printf("[INFO] Starting validation of execution %s", workflowExecution.ExecutionId)
shouldRerun := false
rerun, rerunOk := query["rerun"]
if rerunOk && len(rerun) > 0 && rerun[0] == "true" {
shouldRerun = true
}
if shouldRerun {
log.Printf("[DEBUG] Returning single action execution ID for rerun: %s", workflowExecution.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)))
return
}
log.Printf("[INFO] Starting validation of execution %s", workflowExecution.ExecutionId)
time.Sleep(2 * time.Second)
returnBody := shuffle.HandleRetValidation(ctx, workflowExecution, 1)
returnBytes, err := json.Marshal(returnBody)
if err != nil {
+3
View File
@@ -33,6 +33,9 @@ const AdminNavBar = (props) => {
const navigate = useNavigate();
//const leadinfo = selectedOrganization.lead_info === undefined || selectedOrganization.lead_info === null || selectedOrganization.lead_info === "" ? "" : JSON.stringify(selectedOrganization.lead_info)
//const isPartner = leadinfo.includes("partner")
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams.get('tab');
+4 -4
View File
@@ -891,7 +891,7 @@ const Billing = memo((props) => {
</span>
: null}
{showSupport ?
{/* {showSupport ?
<Button variant="outlined" color="primary" style={{ marginTop: 20, marginBottom: 10, }} onClick={() => {
if (window.drift !== undefined) {
//window.drift.api.startInteraction({ interactionId: 340045 })
@@ -902,7 +902,7 @@ const Billing = memo((props) => {
}}>
Get Support
</Button>
: null}
: null} */}
</div>
)
}
@@ -2021,7 +2021,7 @@ const Billing = memo((props) => {
<div style={{ display: "flex", width: clickedFromOrgTab ? "100%" : "auto", overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'thin', scrollbarColor: '#494949 #2f2f2f', height: isChildOrg ? 0 : "100%", marginTop: 20}} >
<div style={{ display: "flex", flexDirection: "column", width: "100%", }}>
{isCloud &&
{/* {isCloud &&
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 &&
@@ -2043,7 +2043,7 @@ const Billing = memo((props) => {
/>
)
})
: null}
: null} */}
<div style={{ display: "flex", flexDirection: "row", width: "100%", marginTop: 20, marginBottom: 20, maxWidth: 860, }}>
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null :
+10 -6
View File
@@ -151,13 +151,16 @@ const CacheView = memo((props) => {
if (fileCategories.length === 1 && fileCategories[0] === "default") {
var newcategories = ["default"]
for (var key in responseJson.keys) {
if (responseJson.keys[key].category !== undefined && responseJson.keys[key].category !== null && responseJson.keys[key].category !== "" && !fileCategories.includes(responseJson.keys[key].category)) {
newcategories.push(responseJson.keys[key].category);
var category = responseJson.keys[key].category
if (category !== undefined && category !== null && category !== ""){
category = category.replaceAll(" ", "_")
if (!newcategories.includes(category)) {
newcategories.push(category)
}
}
}
console.log("CATEGORIES: ", newcategories)
setFileCategories(newcategories)
}
}
@@ -357,6 +360,7 @@ const CacheView = memo((props) => {
<span style={{ color: "white" }}>
{ editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`}
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: "#212121", }}>
Key
@@ -1041,13 +1045,13 @@ const CacheView = memo((props) => {
</span>
</Tooltip>
<Tooltip
title={data?.org_id !== selectedOrganization.id ? "You can not delete this key as it is controlled by parent organization." : "Delete this key" }
title={selectedOrganization?.id !== undefined && data?.org_id !== selectedOrganization.id ? "You can not delete this key as it is controlled by parent organization." : "Delete this key" }
aria-label={"Delete"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
disabled={selectedOrganization?.id === undefined ? false : data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
deleteCache(orgId, data.key);
//deleteFile(orgId);
+2
View File
@@ -545,6 +545,8 @@ const EditWorkflow = (props) => {
}}
>
<FormControlLabel value="test" control={<Radio />} label="Test" />
<FormControlLabel value="staging" control={<Radio />} label="Staging" />
<FormControlLabel value="preprod" control={<Radio />} label="Pre-production" />
<FormControlLabel value="production" control={<Radio />} label="Production" />
</RadioGroup>
+8 -6
View File
@@ -774,8 +774,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
}, [window?.location?.pathname]);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const showPartnerLogo = userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.image !== undefined && userdata?.active_org?.image !== null && userdata?.active_org?.image.length > 0
return (
<div
style={{
@@ -836,11 +836,13 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
}
}}
>
<Link to="/">
<Link to={isCloud && !showPartnerLogo ? "/" : "/workflows"}>
<img
src={ShuffleLogo}
src={
showPartnerLogo ? userdata?.active_org?.image : ShuffleLogo
}
alt="Shuffle Logo"
style={{ width: 24, height: 24 }}
style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }}
/>
</Link>
</Tooltip>
@@ -1611,7 +1613,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
}}
>
{expandLeftNav &&
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav &&
<Button
variant="outlined"
style={{marginBottom: 15, borderWidth: 2, }}
+447 -203
View File
@@ -25,6 +25,8 @@ import {
CardContent,
ButtonGroup,
DialogContentText,
ToggleButton,
ToggleButtonGroup,
} from "@mui/material";
import { useNavigate, Link } from "react-router-dom";
@@ -87,29 +89,80 @@ const LicencePopup = (props) => {
const [calculatedCores, setCalculatedCores] = useState('600')
const [onpremSelectedValue, setOnpremSelectedValue] = useState(8)
const payasyougo = "Pay as you go"
const [billingCycle, setBillingCycle] = useState("annual")
const [scaleValue, setScaleValue] = useState(
new URLSearchParams(window.location.search).get("app_runs") ||
(userdata?.app_execution_limit / 1000) + 50 || 10
);
useEffect(() => {
setScaleValue((userdata?.app_execution_limit / 1000) + 50 || 10)
}, [userdata])
const getPrice = (basePrice) => {
return Math.round(billingCycle === "annual" ? basePrice * 0.9 : basePrice); // 10% discount for annual
};
const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""
// Handle slider change for Scale plan
const handleScaleChange = (event, newValue) => {
setScaleValue(newValue);
// Add app runs to URL query params
const urlSearchParams = new URLSearchParams(window.location.search);
urlSearchParams.set("app_runs", newValue); // Convert to actual app runs (k to actual number)
const newUrl = `${window.location.pathname}?${urlSearchParams.toString()}`;
window.history.replaceState({}, "", newUrl);
};
// Handle billing cycle change
const handleBillingCycleChange = (event, newValue) => {
if (newValue !== null) {
setBillingCycle(newValue);
if(isCloud){
ReactGA.event({
category: 'Billingpage',
action: 'Billing Cycle Changed',
label: `${billingCycle} -> ${newValue}`,
});
}
// Add billing cycle to URL query params
const urlSearchParams = new URLSearchParams(window.location.search);
urlSearchParams.set("billing_cycle", newValue);
const newUrl = `${
window.location.pathname
}?${urlSearchParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}
};
const payasyougo = "Pay as you go"
const paperStyle = {
padding: 20,
paddingBottom: 30,
borderRadius: theme.palette?.borderRadius,
height: "100%"
}
billingInfo.subscription = {
"active": true,
"name": "Pay as you go",
"price": typecost_single,
"currency": "USD",
"currency_text": "$",
"interval": "app run / month",
"name": userdata?.app_execution_limit === 10000 ? "10,000 App Runs" : "2,000 App Runs",
"price": "Free",
"currency": "Free",
"currency_text": "",
"interval": "",
"description": "Pay as you go",
"features": [
"Basic Support",
"Includes 10.000 app run/month for free. ",
"Pay for what you use with no minimum commitment and cancel anytime."
"Community Support",
`Includes ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} app run/month for free. `,
"Get all 2500+ Apps and 10 Workflows",
"Invite up to 5 users"
],
"limit": 10000,
"limit": userdata?.app_execution_limit === 10000 ? 10000 : 2000,
}
const sendSignatureRequest = (subscription) => {
@@ -151,32 +204,32 @@ const LicencePopup = (props) => {
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
const [hovered, setHovered] = React.useState(false)
const [newBillingEmail, setNewBillingEmail] = useState('');
var top_text = "Base Cloud Access"
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
subscription.name = "Enterprise"
subscription.currency_text = "$"
subscription.price = subscription.level * 180
subscription.limit = subscription.level * 100000
subscription.interval = subscription.recurrence
subscription.features = [
"Includes " + subscription.limit + " app runs/month. ",
"Multi-Tenancy and Region-Selection",
"And all other features from /pricing",
]
}
var top_text = "Starter Plan"
// if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
// subscription.name = "Enterprise"
// subscription.currency_text = "$"
// subscription.price = subscription.level * 180
// subscription.limit = subscription.level * 100000
// subscription.interval = subscription.recurrence
// subscription.features = [
// "Includes " + subscription.limit + " app runs/month. ",
// "Multi-Tenancy and Region-Selection",
// "And all other features from /pricing",
// ]
// }
if (userdata?.app_execution_limit >= 300000) {
subscription.name = "Enterprise"
subscription.currency_text = "$"
subscription.price = typecost_single
subscription.limit = userdata?.app_execution_limit
subscription.interval = "app run / month"
subscription.features = [
"Includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. ",
"Multi-Tenancy and Region-Selection",
"And all other features from /pricing",
]
}
// if (userdata?.app_execution_limit >= 300000) {
// subscription.name = "Enterprise"
// subscription.currency_text = "$"
// subscription.price = typecost_single
// subscription.limit = userdata?.app_execution_limit
// subscription.interval = "app run / month"
// subscription.features = [
// "Includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. ",
// "Multi-Tenancy and Region-Selection",
// "And all other features from /pricing",
// ]
// }
var newPaperstyle = JSON.parse(JSON.stringify(paperStyle))
@@ -189,12 +242,12 @@ const LicencePopup = (props) => {
var showSupport = false
if (subscription.name.includes("default")) {
top_text = "Custom Contract"
newPaperstyle.border = "1px solid #f85a3e"
// newPaperstyle.border = "1px solid #f85a3e"
showSupport = true
}
if (subscription.name.includes("App Run Units")) {
top_text = "Cloud Access"
top_text = "Scale Plan"
showSupport = true
}
@@ -357,7 +410,11 @@ const LicencePopup = (props) => {
</div>
</DialogContent>
</Dialog>
{subscription.active === true && !isScale && <Button style={{ backgroundColor: '#2F2F2F', color: "white", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', width: 144, height: 40 }}>Current Plan</Button> }
{subscription.active === true && !isScale && <Button style={{ backgroundColor: '#2f2f2f', color: "#ffffff", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', fontSize: 13 }}
variant="contained"
color="primary">
Current Plan
</Button>}
<div style={{ display: "flex" }}>
{top_text === "Base Cloud Access" && userdata.has_card_available === true && !isScale ?
<Chip
@@ -381,7 +438,7 @@ const LicencePopup = (props) => {
color="primary"
/>
: null}
<Typography variant="h6" style={{ marginTop: 10, marginBottom: 10, flex: 5, whiteSpace: 'nowrap' }}>
<Typography variant="h6" style={{ marginTop: 25, marginBottom: 10, flex: 5, whiteSpace: 'nowrap' }}>
{top_text}
</Typography>
@@ -396,7 +453,7 @@ const LicencePopup = (props) => {
}}
/>
: null}
{isCloud && highlight === true && top_text !== "Base Cloud Access" ?
{isCloud && highlight === true && top_text !== "Starter Plan" ?
<Tooltip
title="Sign EULA"
placement="top"
@@ -426,7 +483,7 @@ const LicencePopup = (props) => {
{subscription.currency_text}{subscription.price}
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginLeft: 10, marginTop: 15, marginBottom: 10 }}>
/ {subscription.interval}
{subscription.interval.length > 0 ? `/ ${subscription.interval}` : ""}
</Typography>
</div>
: null}
@@ -517,23 +574,17 @@ const LicencePopup = (props) => {
: null}
</ul>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
{subscription.name.includes("Scale") ?
""
:
userdata.has_card_available === true ?
"While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month."
:
{
isCloud ?
userdata?.app_execution_limit && userdata?.app_execution_limit >= 300000 ?
"You have subscribed to the Enterprise plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." :
`You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.`
userdata?.app_execution_limit && userdata?.app_execution_limit !== 10000 ?
"You have already subscribed to the Scale plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." :
`You are using free Starter plan with max ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} runs per month. Upgrade to increase this limit.`
:
`You are not subscribed to any plan and are using the free, open source plan. This plan has no enforced limits, but scale issues may occur due to CPU congestion.`
}
</Typography>
{isCloud && (userdata.has_card_available === true || selectedOrganization?.Billing?.Email?.length > 0 )?
{/* {isCloud && (userdata.has_card_available === true || selectedOrganization?.Billing?.Email?.length > 0 )?
<div>
<span>Billing email: {BillingEmail}</span>
<Button
@@ -611,15 +662,14 @@ const LicencePopup = (props) => {
</DialogActions>
</Dialog>
</div>
: null}
: null} */}
</div>
{isCloud ? (
<Button
fullWidth
disabled={false}
color="primary"
style={{
marginTop: !userdata.has_card_available ? 20 : 10,
marginTop: !userdata.has_card_available ? 25 : 10,
borderRadius: 4,
height: 40,
fontSize: 16,
@@ -630,20 +680,15 @@ const LicencePopup = (props) => {
}}
onClick={() => {
if (isCloud) {
handlePayasyougo(userdata, selectedOrganization, BillingEmail)
//navigate("/pricing?tab=cloud&highlight=true")
} else {
//window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
handlePayasyougo()
}
console.log("Subscription: ", subscription.name)
if(!subscription.name.includes("App Run Units")) {
window.open("https://discord.gg/B2CBzUm", "_blank")
} else {
window.open("mailto:support@shuffler.io", "_blank")
}
}}
>
{userdata.has_card_available === true ?
"Manage Card Details"
:
"Add Card Details"
}
Get Support
</Button> ) : null}
<Button
variant="outlined"
@@ -709,42 +754,42 @@ const LicencePopup = (props) => {
)
}
useEffect(() => {
console.log("New variant: ", shuffleVariant)
// useEffect(() => {
// console.log("New variant: ", shuffleVariant)
if (shuffleVariant === 1) {
setCalculatedCost("$960")
setSelectedValue(8)
} else {
if (userdata && userdata?.app_execution_limit) {
if (userdata.app_execution_limit >= 300000 && userdata.app_execution_limit < 400000) {
setSelectedValue(400)
setCalculatedCost("$1280")
}else if (userdata?.app_execution_limit >= 400000 && userdata?.app_execution_limit < 500000) {
setSelectedValue(500)
setCalculatedCost("$1600")
} else if (userdata?.app_execution_limit >= 500000 && userdata?.app_execution_limit < 600000) {
setSelectedValue(600)
setCalculatedCost("$1920")
} else if (userdata?.app_execution_limit >= 600000 && userdata?.app_execution_limit < 700000) {
setSelectedValue(700)
setCalculatedCost("$2240")
} else if (userdata?.app_execution_limit >= 700000 && userdata?.app_execution_limit < 800000) {
setSelectedValue(800)
setCalculatedCost("$2560")
} else if (userdata?.app_execution_limit >= 800000 && userdata?.app_execution_limit < 900000) {
setSelectedValue(900)
setCalculatedCost("$2880")
}else {
setCalculatedCost("$960")
setSelectedValue(300)
}
}else {
setCalculatedCost("$960")
setSelectedValue(300)
}
}
}, [shuffleVariant])
// if (shuffleVariant === 1) {
// setCalculatedCost("$960")
// setSelectedValue(8)
// } else {
// if (userdata && userdata?.app_execution_limit) {
// if (userdata.app_execution_limit >= 30000 && userdata.app_execution_limit < 40000) {
// setSelectedValue(400)
// setCalculatedCost("$1280")
// }else if (userdata?.app_execution_limit >= 40000 && userdata?.app_execution_limit < 50000) {
// setSelectedValue(500)
// setCalculatedCost("$1600")
// } else if (userdata?.app_execution_limit >= 500000 && userdata?.app_execution_limit < 600000) {
// setSelectedValue(600)
// setCalculatedCost("$1920")
// } else if (userdata?.app_execution_limit >= 60000 && userdata?.app_execution_limit < 70000) {
// setSelectedValue(700)
// setCalculatedCost("$2240")
// } else if (userdata?.app_execution_limit >= 70000 && userdata?.app_execution_limit < 80000) {
// setSelectedValue(800)
// setCalculatedCost("$2560")
// } else if (userdata?.app_execution_limit >= 80000 && userdata?.app_execution_limit < 90000) {
// setSelectedValue(900)
// setCalculatedCost("$2880")
// }else {
// setCalculatedCost("$960")
// setSelectedValue(300)
// }
// }else {
// setCalculatedCost("$960")
// setSelectedValue(300)
// }
// }
// }, [userdata])
if (typeof window === 'undefined' || window.location === undefined) {
return null
@@ -900,71 +945,121 @@ const LicencePopup = (props) => {
}
console.log("Priceitem: ", shuffleVariant)
// const isLoggedInHandler = () => {
// if (calculatedCost === payasyougo) {
// handlePayasyougo(props.userdata)
// return
// }
// const priceItem =
// window.location.origin === "https://shuffler.io/" || "https://sandbox.shuffler.io/"
// ? shuffleVariant === 0
// ? "price_1PWI3uDzMUgUjxHSffUBwWCy"
// : "price_1PWI8EDzMUgUjxHSfEhUB7oL"
// : shuffleVariant === 0
// ? "price_1PZPSSEJjT17t98NLJoTMYja"
// : "price_1PZPQuEJjT17t98N3yORUtd9";
// const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
// const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
// const quantity = shuffleVariant === 0 ? selectedValue / 100 : selectedValue
// console.log("Priceitem: ", priceItem, quantity, shuffleVariant)
// var checkoutObject = {
// lineItems: [
// {
// price: priceItem,
// quantity: quantity,
// },
// ],
// mode: "subscription",
// billingAddressCollection: "auto",
// successUrl: successUrl,
// cancelUrl: failUrl,
// clientReferenceId: props.userdata.active_org.id,
// }
// if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) {
// window.open("https://shuffler.io/admin?admin_tab=billingstats&payment=stripe_error", "_self")
// }
// stripe.redirectToCheckout(checkoutObject)
// .then(function (result) {
// console.log("SUCCESS STRIPE?: ", result)
// ReactGA.event({
// category: "pricing",
// action: "add_card_success",
// label: "",
// })
// })
// .catch(function (error) {
// console.error("STRIPE ERROR: ", error)
// ReactGA.event({
// category: "pricing",
// action: "add_card_error",
// label: "",
// })
// })
// }
const isLoggedInHandler = () => {
if (calculatedCost === payasyougo) {
handlePayasyougo(props.userdata)
return
var priceItem;
if (window.location.origin === "https://shuffler.io" || window.location.origin === "https://sandbox.shuffler.io") {
priceItem = billingCycle === "monthly" ? "price_1R66rbEJjT17t98NHIQ78nrz" : "price_1R671UEJjT17t98NzfqWvSG7"
} else if (window.location.origin === "http://localhost:3002") {
priceItem = billingCycle === "monthly" ? "price_1R678hEJjT17t98Nai5J50gs" : "price_1R6c84EJjT17t98NR68gUfT7"
}
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`;
const failUrl = `${window.location.origin}/pricing?admin_tab=billingstats&payment=failure`;
let quantity;
if (billingCycle === "monthly") {
quantity = scaleValue / 10
} else {
quantity = (scaleValue / 10) * 12
}
redirectToCheckout(priceItem, quantity, successUrl, failUrl);
};
const redirectToCheckout = (priceItem, quantity, successUrl, failUrl) => {
const checkoutObject = {
lineItems: [
{
price: priceItem,
quantity: quantity,
},
],
mode: "subscription",
billingAddressCollection: "auto",
successUrl: successUrl,
cancelUrl: failUrl,
clientReferenceId: userdata.active_org.id,
};
console.log("OBJECT: ", priceItem, checkoutObject);
stripe
.redirectToCheckout(checkoutObject)
.then(function (result) {
console.log("SUCCESS STRIPE?: ", result);
})
.catch(function (error) {
console.error("STRIPE ERROR: ", error);
});
};
const priceItem =
window.location.origin === "https://shuffler.io/" || "https://sandbox.shuffler.io/"
? shuffleVariant === 0
? "price_1PWI3uDzMUgUjxHSffUBwWCy"
: "price_1PWI8EDzMUgUjxHSfEhUB7oL"
: shuffleVariant === 0
? "price_1PZPSSEJjT17t98NLJoTMYja"
: "price_1PZPQuEJjT17t98N3yORUtd9";
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
const quantity = shuffleVariant === 0 ? selectedValue / 100 : selectedValue
console.log("Priceitem: ", priceItem, quantity, shuffleVariant)
var checkoutObject = {
lineItems: [
{
price: priceItem,
quantity: quantity,
},
],
mode: "subscription",
billingAddressCollection: "auto",
successUrl: successUrl,
cancelUrl: failUrl,
clientReferenceId: props.userdata.active_org.id,
}
if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) {
window.open("https://shuffler.io/admin?admin_tab=billingstats&payment=stripe_error", "_self")
}
stripe.redirectToCheckout(checkoutObject)
.then(function (result) {
console.log("SUCCESS STRIPE?: ", result)
ReactGA.event({
category: "pricing",
action: "add_card_success",
label: "",
})
})
.catch(function (error) {
console.error("STRIPE ERROR: ", error)
ReactGA.event({
category: "pricing",
action: "add_card_error",
label: "",
})
})
}
console.log("Selected Organization: ", selectedOrganization.subscriptions)
return (
<div>
<Grid container spacing={2} columns={16} style={{ flexDirection: "row", flexWrap: "nowrap", borderRadius: '16px', display: "flex", }}>
<Grid container spacing={2} columns={16} style={{ flexDirection: "row", flexWrap: "nowrap", borderRadius: '16px', display: "flex"}}>
<Grid item xs={8}>
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ?
{selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0 ?
<SubscriptionObject
index={0}
globalUrl={globalUrl}
@@ -976,7 +1071,29 @@ const LicencePopup = (props) => {
subscription={billingInfo.subscription}
highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0}
/>
: !isCloud ?
:
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 ?
selectedOrganization.subscriptions
.reverse()
.map((sub, index) => {
return (
<SubscriptionObject
index={index + 1}
globalUrl={globalUrl}
userdata={userdata}
serverside={serverside}
billingInfo={billingInfo}
stripeKey={stripeKey}
selectedOrganization={selectedOrganization}
subscription={sub}
highlight={true}
/>
)
})
: null}
{!isCloud ?
<span style={{ display: "flex", }}>
<SubscriptionObject
index={0}
@@ -1044,61 +1161,184 @@ const LicencePopup = (props) => {
})
: null} */}
</Grid>
<Grid item xs={8}>
<Grid item xs={8} sx={{ height: "100%" }}>
<Grid style={{}}>
{errorMessage.length > 0 ? <Typography variant="h4">Error: {errorMessage}</Typography> : null}
<Card style={{
padding: 20,
borderRadius: theme.palette?.borderRadius,
border: !isScale ? "1px solid #f85a3e" : 'none',
background:
"linear-gradient(to right, #212121, #212121) padding-box, linear-gradient(90deg, #F86744 0%, #F34475 100%) border-box",
borderWidth: "2px",
borderStyle: "solid",
borderColor: "transparent",
}}>
<div>
{ !isScale && <Button style={{ backgroundColor: 'rgba(255, 132, 68, 0.2)', color: "#FF8444", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', }}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 25,
marginLeft: -2,
}}
>
<Button style={{ backgroundColor: 'rgba(255, 132, 68, 0.2)', color: "#FF8444", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', fontSize: 13 }}
variant="contained"
color="primary">Recommended </Button> }
color="primary">Recommended
</Button>
{
billingCycle === "annual" &&
(
<Box
sx={{
background: "rgba(248, 103, 68, 0.1)",
py: 0.5,
px: 1.5,
borderRadius: "8px",
}}
>
<Typography
sx={{
fontWeight: "bold",
background:
"linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
fontSize: {
xs: "12px",
md: "14px",
},
}}
>
10% OFF
</Typography>
</Box>
)
}
</div>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 10,
marginTop: 10,
}}
>
<Typography variant="h6" style={{ }}>
{scaleValue > 300 ? "Enterprise Plan" : "Scale Plan"}
</Typography>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<ToggleButtonGroup
value={billingCycle}
exclusive
onChange={handleBillingCycleChange}
aria-label="billing cycle"
sx={{
backgroundColor: "rgba(255, 255, 255, 0.1)",
fontFamily: theme.typography.fontFamily,
borderRadius: "30px",
marginTop: -1,
padding: "3px",
"& .MuiToggleButton-root": {
border: "none",
borderRadius: "30px",
color: "#fff",
padding: "6px 22px",
textTransform: "none",
fontSize: {
xs: "12px",
},
"&.Mui-selected": {
backgroundColor: "#fff",
fontFamily: theme.typography.fontFamily,
color: "#1A1A1A",
fontWeight: "bold",
"&:hover": {
backgroundColor: "#fff",
},
},
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.2)",
},
},
}}
>
<ToggleButton value="monthly" aria-label="monthly">
Monthly
</ToggleButton>
<ToggleButton value="annual" aria-label="annual">
Annual
</ToggleButton>
</ToggleButtonGroup>
</Box>
</div>
<Typography variant="h6" style={{ marginTop: 10, marginBottom: 10, flex: 5, }}>{shuffleVariant === 1 ? "Scale" : "Enterprise"}</Typography>
<Divider />
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20 }}>
{shuffleVariant === 0 ?
"SaaS / Cloud - Per Month"
:
"Open Source + Scale License"
}
App Runs Units
</Typography>
<Typography style={{ minHeight: 46, cursor: calculatedCores === "Get A Quote" ? "pointer" : "inherit", }} onClick={() => {
if (calculatedCores === "Get A Quote") {
console.log("Clicked on get a quote")
if (window.drift !== undefined) {
window.drift.api.startInteraction({ interactionId: 340785 })
}
}
}}>{calculatedCost}</Typography>
<Typography variant="body1" color="textSecondary" style={{}}>For {shuffleVariant === 1 ? `${selectedValue} CPU cores` : `${selectedValue}k App Runs`}: </Typography>
<div style={{ textAlign: "center" }}>
<Slider
aria-label="Small steps"
style={{ width: "80%", margin: "auto" }}
onChange={(event, newValue) => {
handleChange(event, newValue)
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 10 , marginTop: 10 }}>
<Typography style={{
fontSize: 24,
marginTop: 7,
marginBottom: 10,
fontWeight: "500",
}}
>
{scaleValue > 300 ? "Let's Talk" : `$${getPrice(32) * (scaleValue / 10)}`}
</Typography>
<Typography
color="text.secondary"
sx={{
fontSize: "14px",
marginBottom: "-2px",
marginLeft: scaleValue > 300 ? 1 : 0,
}}
marks
value={selectedValue}
step={shuffleVariant === 0 ? 100 : 4}
min={shuffleVariant === 0 ? 100 : 8}
max={shuffleVariant === 0 ? 1000 : 32}
valueLabelDisplay="auto"
/>
>
{scaleValue > 300 ? `for ${scaleValue > 500 ? "500k+" : `${scaleValue}k`} App Runs` : `/month for ${scaleValue}k App Runs`}
</Typography>
</div>
<Box sx={{ px: 1 }}>
<Slider
value={scaleValue}
onChange={handleScaleChange}
aria-labelledby="scale-slider"
valueLabelDisplay="auto"
valueLabelFormat={(value) => {
if(value === 510){
return "500k+"
}
return `${value}k`
}}
step={10}
min={10}
max={510}
marks
sx={{
color: "#ff8544",
"& .MuiSlider-thumb": {
width: 15,
height: 15,
},
"& .MuiSlider-valueLabel": {
backgroundColor: "rgba(33, 33, 33, 1)",
color: "rgba(241, 241, 241, 1)",
fontSize: 14,
borderRadius: "4px",
border: "1px solid rgba(73, 73, 73, 1)",
fontFamily: theme?.typography?.fontFamily,
},
}}
/>
</Box>
<div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{defaultTaskIcon}</span>
<Typography style={{ fontSize: 14, marginLeft: 8 }}>Priority Support</Typography>
<Typography style={{ fontSize: 14, marginLeft: 8 }}>Standard Email Support</Typography>
</div>
<Divider />
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1117,7 +1357,7 @@ const LicencePopup = (props) => {
<Divider />
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{defaultTaskIcon}</span>
<Typography style={{ fontSize: 14, marginLeft: 8 }}>Help with Workflow and App development</Typography>
<Typography style={{ fontSize: 14, marginLeft: 8 }}>30 Days workflow run history</Typography>
</div>
</div>
<div style={{ marginTop: 20, }} />
@@ -1136,7 +1376,7 @@ const LicencePopup = (props) => {
navigate("/pricing")
} else {
window.open("https://shuffler.io/pricing?tab=onprem", "_blank")
window.open("https://shuffler.io/pricing?tab=Self-Hosted", "_blank")
}
}}
color="primary"
@@ -1149,6 +1389,10 @@ const LicencePopup = (props) => {
style={{ borderRadius: 4, textTransform: "capitalize", color: "#1a1a1a", backgroundColor: "#ff8544", width: "100%", fontSize: 16}}
onClick={() => {
if (isCloud) {
if(scaleValue > 300){
navigate("/contact?category=cloud_enterprise_plan")
return;
}
ReactGA.event({
category: "header",
action: "upgread_clicks_popup",
@@ -1170,7 +1414,7 @@ const LicencePopup = (props) => {
}}
color="primary"
>
Upgrade
{scaleValue > 300 ? "Let's Talk" : "Upgrade"}
</Button>
</div>
</DialogActions>
+3 -3
View File
@@ -61,7 +61,7 @@ const menuData = {
description:
"Connect and run actions seamlessly between different platforms.",
icon: "/images/logos/singul.svg",
path: "https://singul.io/",
path: "https://singul-docs.gitbook.io/singul/getting-started",
gaData: {
category: "navbar",
action: "products_click",
@@ -1247,7 +1247,7 @@ const Navbar = (props) => {
letterSpacing: '0.5px',
}}
>
Coming Soon
Beta: Coming Soon
</Typography>
)}
</Box>
@@ -1370,7 +1370,7 @@ const Navbar = (props) => {
label: "go_to_pricing",
})
} else {
window.open("https://shuffler.io/pricing", '_blank');
window.open("https://shuffler.io/pricing?env=Self-Hosted", '_blank');
return;
}
}}
@@ -538,7 +538,7 @@ const OrgHeaderexpandedNew = (props) => {
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "old customer", "old lead"].map((name) => (
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "integration partner", "distribution partner", "service partner", "old customer", "old lead"].map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
+2 -2
View File
@@ -120,7 +120,7 @@ const OrganizationTab = (props) => {
isLoaded={isLoaded}
/>
);
case 'branding(beta)':
case 'branding':
return <Branding
isCloud={isCloud}
userdata={userdata}
@@ -140,7 +140,7 @@ const OrganizationTab = (props) => {
return (
<div style={{ height: "100%", width: "100%", color: '#FFFFFF', backgroundColor: '#212121', borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", boxSizing: 'border-box' }}>
<div style={{ display: 'flex', justifyContent: 'space-around', width: "100%", borderBottom: '1px solid #494949' ,boxSizing: 'border-box' }}>
{['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding (Beta)'].map((tabName, index) => (
{['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding'].map((tabName, index) => (
<Tooltip
key={index}
title={
+101 -14
View File
@@ -179,6 +179,7 @@ const ParsedAction = (props) => {
suborgWorkflows,
originalWorkflow,
runFromHere,
} = props;
let navigate = useNavigate()
@@ -233,11 +234,16 @@ const ParsedAction = (props) => {
// auth, required, optional
var changed = false
if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) {
console.log("Returning because no params")
return
}
if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated !== true) {
return
if (isAgent || isIntegration) {
} else {
return
}
}
// Fixing required fields with a shitty structure :)
@@ -346,13 +352,23 @@ const ParsedAction = (props) => {
// 3. generated fields & all else
const newparams = auth
var newparams = auth
.concat(bodyfield)
.concat(required)
.concat(generated_optional)
.concat(special_optional)
.concat(optional)
const dedupedParams = []
for (var paramKey in newparams) {
const param = newparams[paramKey]
if (dedupedParams.find(item => item.name === param.name) === undefined) {
dedupedParams.push(param)
}
}
newparams = dedupedParams
var newkeyorder = []
for (let paramkey in newparams) {
//console.log("Param: ", newparams[paramkey])
@@ -666,11 +682,14 @@ const ParsedAction = (props) => {
// Process workflowExecutions
if (workflowExecutions.length > 0) {
var appended = false
var foundvalue = ""
for (let execution of workflowExecutions) {
const execArg = execution.execution_argument;
if (execArg && execArg.length > 0) {
const valid = validateJson(execArg);
if (valid.valid) {
appended = true
newActionList.push({
type: "Runtime Argument",
name: "Runtime Argument",
@@ -681,9 +700,23 @@ const ParsedAction = (props) => {
})
break
} else {
foundvalue = execArg
}
}
}
if (!appended && foundvalue !== undefined && foundvalue !== "") {
newActionList.push({
type: "Runtime Argument",
name: "Runtime Argument",
highlight: "exec",
autocomplete: "exec",
value: foundvalue,
example: foundvalue,
})
}
}
// Add default Runtime Argument if none were added
@@ -772,6 +805,8 @@ const ParsedAction = (props) => {
}
labels.push(parentNode.label);
var secondaryExample = ""
let exampleData = parentNode.example ?? "";
if (parentNode?.app_name === "http") {
exampleData = ""
@@ -782,9 +817,22 @@ const ParsedAction = (props) => {
const foundResult = exec.results?.find(result => result?.action?.id === parentNode?.id);
if (foundResult) {
const valid = validateJson(foundResult.result);
if (valid.valid && valid.result.success !== false) {
exampleData = valid.result
break
if (valid.valid) {
// Check if array, and if first item is object + success
if (Array.isArray(valid.result) && valid.result.length > 0 && typeof valid.result[0] === "object") {
if (valid.result[0].success !== false) {
exampleData = valid.result[0]
break
}
} else {
if (valid.result.success !== false) {
exampleData = valid.result
break
}
}
} else {
secondaryExample = foundResult.result
}
}
}
@@ -818,6 +866,10 @@ const ParsedAction = (props) => {
}
}
}
}
if (exampleData === "" && secondaryExample !== "") {
exampleData = secondaryExample
}
if (parentNode.label === undefined) {
@@ -1162,7 +1214,9 @@ const ParsedAction = (props) => {
selectedAction.parameters[1].value = splitparsed[1]
if (splitparsed.length > 2) {
toast.warn("Filter list only supports filtering at the first level. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.")
toast.warn("Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", {
autoClose: 10000,
})
} else if (selectedAction.parameters[1].value.includes(".#")) {
toast.warn("This filter may not work due to using .# indexing. Please use the 'execute python' action and try the 'filter a list' function in the code editor.")
}
@@ -1814,6 +1868,38 @@ const ParsedAction = (props) => {
</Tooltip>
</IconButton>
<Tooltip
title={
<Typography variant="body2" style={{margin: 3, }}>
Rerun this action with results from previous executions. Built for testing individual actions in the middle of workflows.
</Typography>
}
placement="top"
>
<Button
color="secondary"
variant="outlined"
style={{
marginTop: "auto",
marginBottom: "auto",
height: 30,
marginLeft: 115,
textTransform: "none",
}}
disabled={autoCompleting}
onClick={() => {
if (runFromHere !== undefined) {
runFromHere(selectedAction)
} else {
toast.error("Function not available. Please contact support@shuffler.io")
}
}}
>
<PlayArrowIcon style={{marginRight: 5, }}/>
Rerun
</Button>
</Tooltip>
{(selectedAction?.generated === true && selectedAction?.app_version === "1.0.0") || (selectedAction?.app_name === "Shuffle Tools" && selectedAction?.app_version !== "1.2.0") ?
<Button
variant="contained"
@@ -1875,12 +1961,12 @@ const ParsedAction = (props) => {
toast.success("Changed version of all nodes to " + event.target.value)
}}
style={{
marginTop: 10,
position: "absolute",
top: 10, right: 10,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.inputColor,
color: "white",
height: 35,
marginleft: 10,
borderRadius: theme.palette?.borderRadius,
}}
SelectDisplayProps={{
@@ -4120,15 +4206,12 @@ const ParsedAction = (props) => {
fullWidth
id={"rightside_field_" + count}
onChange={(e) => {
console.log("MULTI SELECT: ", multi, e.target.value)
changeActionParameter(e, count, data);
setUpdate(Math.random());
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: "50px",
backgroundColor: theme.palette.platformColor,
height: 40,
borderRadius: theme.palette?.borderRadius,
}}
>
@@ -4525,6 +4608,7 @@ const ParsedAction = (props) => {
minWidth: 250,
maxWidth: 250,
marginRight: 0,
paddingLeft: 12,
}}
value={innerdata}
onMouseOver={() => handleMouseover()}
@@ -4685,8 +4769,9 @@ const ParsedAction = (props) => {
</Tooltip>
: null}
<Tooltip title="Expand editor window" placement="top">
{((data.options !== undefined && data.options !== null && data.options.length > 0) || (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) ? null :
<Tooltip title="Expand editor window" placement="top">
<OpenInFullIcon
style={{ color: "rgba(255,255,255,0.7)", cursor: "pointer", margin: multiline ? 5 : 0, height: 20, width: 20, }}
onMouseOver={(event) => {
@@ -4728,6 +4813,7 @@ const ParsedAction = (props) => {
}}
/>
</Tooltip>
}
</div>
@@ -4737,6 +4823,7 @@ const ParsedAction = (props) => {
showDropdownNumber === count &&
data.variant === "STATIC_VALUE" &&
jsonList.length > 0 ? (
<FormControl fullWidth style={{ marginTop: 0 }}>
<InputLabel
id="action-autocompleter"
+2 -1
View File
@@ -837,12 +837,13 @@ const NotificationItem = memo((props) => {
color="secondary"
style={{
height: 50,
textTransform: "none",
}}
onClick={() => {
dismissNotification(data.id);
}}
>
Mark Read
Mark as Read
</Button>
) : null}
+98 -13
View File
@@ -46,6 +46,7 @@ import {
DragIndicator as DragIndicatorIcon,
RestartAlt as RestartAltIcon,
ArrowForward as ArrowForwardIcon,
KeyboardReturn as KeyboardReturnIcon,
} from '@mui/icons-material';
@@ -94,9 +95,12 @@ const pythonFilters = [
{ "name": "Get full execution details", "value": `print(self.full_execution)`, "example": `` },
{ "name": "Use files", "value": `# Create a sample file\nfiles = [{\n \"filename\": \"test.txt\",\n \"data\": \"Testdata\"\n}]\nret = self.set_files(files)\n\n# Get the content of the file from Shuffle storage\n# Originally a byte string in the \"data\" key\nfile_content = (self.get_file(ret[0])[\"data\"]).decode()\nprint(file_content)`, "example": `` },
{ "name": "Use datastore", "value": `key = \"testkey\"\nvalue = \"The value of the testkey\"\n\nself.set_cache(key, value)\n\n# Print the details of the key after it's been updated\n# To get the value, use self.get_cache(key)[\"value\"]\nprint(self.get_cache(key))`, "example": `` },
{ "name": "Run an App Action", "value": `response = shuffle.run_app(app_id="app", action="action_name", auth="authentication_id", params={})\nprint(response)`, "example": ``, "disabled": true, },
{ "name": "Run a Singul AI Action", "value": `response = singul.create_ticket(app="jira/iris/ticketingsystem", fields={"title": "Test ticket!"})\nprint(response)`, "example": ``, "disabled": true, },
{ "name": "Use datastore", "value": `key = \"testkey\"\nvalue = \"The value of the testkey\"\n\nself.set_key(key, value)\n\n# Print the details of the key after it's been updated\n# To get the value, use self.get_key(key)[\"value\"]\nprint(self.get_key(key))`, "example": `` },
{ "name": "Run a Subflow", "value": `response = shuffle.run_workflow(workflow_id="", start_command="Runtime arg here!", wait=True)\nprint(response)`, "example": ``, "disabled": false, },
{ "name": "Run an App Action", "value": `response = shuffle.run_app(app_id="app", action="action_name", auth="authentication_id", params={})\nprint(response)`, "example": ``, "disabled": false, },
{ "name": "Run a Singul AI Action", "value": `response = singul.cases.create_ticket(app="jira", fields={"title": "Test ticket!"})\nprint(response)`, "example": ``, "disabled": false, },
]
@@ -299,8 +303,19 @@ const CodeEditor = (props) => {
setMainVariables(tmpVariables)
}
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault()
const tryItButton = document.getElementById("try-it-button")
if (tryItButton !== undefined && tryItButton !== null) {
tryItButton.click()
}
}
}
// Remove the original useEffect for actionlist since we'll update on action/trigger changes
useEffect(() => {
document.addEventListener("keydown", handleKeyDown)
updateAvailableVariables(actionlist)
}, [])
@@ -887,6 +902,8 @@ const CodeEditor = (props) => {
// Whelp this is inefficient af. Single loop pls
// When the found array is empty.
if (found !== null && found !== undefined) {
//console.log("FOUND: ", found)
try {
for (var i = 0; i < found.length; i++) {
try {
@@ -921,28 +938,72 @@ const CodeEditor = (props) => {
}
}
}
// Find the location to ensure replacements happen correctly
var foundlocation = -1
for (var j = 0; j < input.length; j++) {
const foundStringSize = fixedVariable.length
const foundslice = input.slice(j, j + foundStringSize)
//console.log("FOUNDSLICE: ", foundslice)
if (fixedVariable !== foundslice) {
continue
}
// Check if it matches EXACTLY or not, as there may be more AFTER the found[i]
const nextchar = input.slice(j + foundStringSize, j + foundStringSize + 1)
if (nextchar === ".") {
continue
}
foundlocation = j
break
}
// FIXME: There is something wrong here with:
// $variable.#
// vs
// $variable.#.subvalue
// if you put both of those lines in the same editor, then it will replace both (somehow). Make sure $variable.#.subvalue exists while testing.
console.log("FOUNDLOC: ", fixedVariable, foundlocation)
for (var j = 0; j < actionlist.length; j++) {
if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) {
continue
}
// Look for the location of found[i] in the input, as to make sure to skip parts of the input in the replace. Find ALL spots for it
valuefound = true
var newvalue = ""
try {
if (typeof actionlist[j].example === "object") {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
if (typeof actionlist[j].example === "object") {
newvalue = JSON.stringify(actionlist[j].example)
} else if (actionlist[j].example.trim().startsWith("{") || actionlist[j].example.trim().startsWith("[")) {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
newvalue = JSON.stringify(actionlist[j].example)
} else {
const newExample = fixStringInput(actionlist[j].example)
input = input.replace(found[i], newExample, -1)
newvalue = newExample
}
} catch (e) {
input = input.replace(found[i], actionlist[j].example, -1)
newvalue = actionlist[j].example
}
try {
console.log("REPLACE: ", foundlocation, fixedVariable, newvalue)
if (newvalue !== "") {
if (foundlocation === -1) {
input = input.replace(fixedVariable, newvalue, 1)
} else {
// Ensures we don't just randomly replace the first value we find
const replacedSlice = input.slice(foundlocation, input.length).replace(fixedVariable, newvalue, 1)
input = input.slice(0, foundlocation) + replacedSlice
}
}
} catch (e) {
console.log("Replace error: ", e)
}
}
if (!valuefound) {
@@ -1721,13 +1782,13 @@ const CodeEditor = (props) => {
color="secondary"
style={{
textTransform: "none",
width: 120,
width: 145,
}}
onClick={(event) => {
setAnchorEl3(event.currentTarget);
}}
>
Python Code
Python Examples
</Button>
<Button
id="basic-button"
@@ -1762,7 +1823,12 @@ const CodeEditor = (props) => {
>
{pythonFilters.map((item, index) => {
return (
<MenuItem key={index} onClick={() => {
<MenuItem
style={{
borderTop: item.name === "Use files" || (item.name.toLowerCase().includes("run") && item.name.toLowerCase().includes("subflow")) ? "2px solid rgba(255,255,255,0.3)" : "none",
}}
key={index} onClick={() => {
if (item.disabled) {
toast.error("This feature may not work in your environment until you update your Shuffle Tools app.", { autoClose: 10000 })
}
@@ -2270,6 +2336,7 @@ const CodeEditor = (props) => {
<div style={{}}>
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' or 'execute python' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
<Button
id="try-it-button"
variant="outlined"
disabled={executing}
color="primary"
@@ -2283,11 +2350,13 @@ const CodeEditor = (props) => {
zIndex: 1200,
fontWeight: 500,
fontSize: 14,
textTransform: "none",
backgroundColor: "rgba(33, 33, 33, 0.95)",
backdropFilter: "blur(8px)",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)",
transition: "all 0.2s ease",
borderRadius: "4px",
paddingRight: 20,
borderRadius: theme.palette?.borderRadius,
"&:hover": {
backgroundColor: "rgba(45, 45, 45, 0.95)",
transform: "translateY(-1px)",
@@ -2304,7 +2373,23 @@ const CodeEditor = (props) => {
{executing ?
<CircularProgress style={{ height: 18, width: 18, }} />
:
<span>{selectedAction === undefined ? "Try it" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}<PlayArrowIcon style={{ height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
<span>
<PlayArrowIcon style={{ height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} />
{selectedAction === undefined ? "Try it" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}
<span
style={{
color: "#C8C8C8",
fontSize: "12px",
whiteSpace: "nowrap",
marginLeft: 5,
marginRight: 10,
}}
>
<kbd>Ctrl</kbd> + <kbd><KeyboardReturnIcon style={{width: 13, position: "absolute", marginLeft: 3, top: 5, }}/></kbd>
</span>
</span>
}
</Button>
</Tooltip>
+492 -399
View File
@@ -30,6 +30,8 @@ import {
Apps as AppsIcon,
Business as BusinessIcon,
Flag,
ArrowDropDown as ArrowDropDownIcon,
} from "@mui/icons-material";
import { toast } from 'react-toastify';
@@ -54,9 +56,12 @@ const TenantsTab = memo((props) => {
const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false);
const [modalOpen, setModalOpen] = React.useState(false);
const [parentOrg, setParentOrg] = React.useState(null);
const [parentOrgFlag, setParentOrgFlag] = React.useState(null);
const [parentOrgFlag, setParentOrgFlag] = React.useState("gb");
const [parentOrgRegionName, setParentOrgRegionName] = React.useState("UK");
const [loadOrgs, setLoadOrgs] = React.useState(true);
const [, forceUpdate] = React.useState();
const [suborglistOpen, setSuborglistOpen] = React.useState(false);
const [allTenantsOpen, setAllTenantsOpen] = React.useState(false);
const itemColor = "black";
useEffect(() => {
@@ -82,9 +87,10 @@ const TenantsTab = memo((props) => {
}
}
setParentOrgFlag(regionCode);
setParentOrgRegionName(regiontag);
}
}
}, [parentOrg]);
}, [parentOrg, parentOrgFlag]);
var syncList = [
{
@@ -164,7 +170,8 @@ const TenantsTab = memo((props) => {
regionCode = "ca";
}
}
setParentOrgFlag(regionCode);
setParentOrgFlag(regionCode);
setParentOrgRegionName(regiontag);
}
}
})
@@ -479,8 +486,8 @@ const TenantsTab = memo((props) => {
const createSubOrg = (currentOrgId, name) => {
const data = { name: name, org_id: currentOrgId };
console.log(data);
const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org`;
setSuborglistOpen(true)
fetch(url, {
mode: "cors",
@@ -788,7 +795,7 @@ const TenantsTab = memo((props) => {
Create, manage and change to sub-organizations (tenants)! {" "}
{isCloud
? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out."
: ''}
: ''}&nbsp;
<a
href="/docs/organizations"
target="_blank"
@@ -1026,7 +1033,7 @@ const TenantsTab = memo((props) => {
src={`https://flagcdn.com/w20/${parentOrgFlag}.png`}
style={{ width: "30px", height: "20px", marginRight: "5px" }}
/>
<ListItemText primary={parentOrgFlag?.toUpperCase()} />
<ListItemText primary={parentOrgRegionName?.toUpperCase()} />
</div>
}
style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }}
@@ -1147,168 +1154,212 @@ const TenantsTab = memo((props) => {
overflowX: "auto",
paddingBottom: 0,
}}>
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
}}>
<ListItemText
primary="Logo"
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}} />
<ListItemText
primary="Name"
style={{
minWidth: 100,
maxWidth: 300,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
textAlign: "center",
}} />
{isCloud && (
<ListItemText
primary="Region"
style={{
minWidth: 100,
maxWidth: 100,
display: "table-cell",
borderBottom: "1px solid #494949",
padding: "0px 8px 8px 8px",
}}
/>
)}
<ListItemText
primary="id"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Action"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
{subOrgs.map((data, index) => {
let regiontag = "UK";
let regionCode = "gb";
{!suborglistOpen ?
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
itemAlign: "center",
if (data.region_url?.length > 0) {
const regionsplit = data.region_url.split(".");
if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) {
const namesplit = regionsplit[0].split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
}}
>
<ListItemText
primary={
<Button
fullWidth
variant="secondary"
style={{
textTransform: 'none',
}}
onClick={() => setSuborglistOpen(true)}
>
Show Sub-Organizations <ArrowDropDownIcon />
</Button>
}
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
:
<span>
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
}}>
<ListItemText
primary="Logo"
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}} />
<ListItemText
primary="Name"
style={{
minWidth: 100,
maxWidth: 300,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
textAlign: "center",
}} />
{isCloud && (
<ListItemText
primary="Region"
style={{
minWidth: 100,
maxWidth: 100,
display: "table-cell",
borderBottom: "1px solid #494949",
padding: "0px 8px 8px 8px",
}}
/>
)}
<ListItemText
primary="id"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Action"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
{subOrgs.map((data, index) => {
let regiontag = "UK";
let regionCode = "gb";
if (data.region_url?.length > 0) {
const regionsplit = data.region_url.split(".");
if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) {
const namesplit = regionsplit[0].split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
}
}
}
}
return (
<ListItem key={index} style={{ backgroundColor: index % 2 === 0 ? '#1A1A1A' : '#212121', width: "100%", borderBottomLeftRadius: 8, display:'table-row', borderBottomRightRadius: 8 }}>
<ListItemText primary={<img alt={data?.name} src={data.image || theme.palette.defaultImage} style={imageStyle} />} style={{ width: 100,
minWidth: 100,
maxWidth: 100,
display: "table-cell",
padding: "8px 8px 8px 20px",
textAlign: "center", }} />
<ListItemText primary={data.name} style={{ minWidth: 100,
maxWidth: 300,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
display: "table-cell",
padding: 8,
verticalAlign: "middle",
textAlign: "center", }} />
return (
<ListItem key={index} style={{ backgroundColor: index % 2 === 0 ? '#1A1A1A' : '#212121', width: "100%", borderBottomLeftRadius: 8, display:'table-row', borderBottomRightRadius: 8 }}>
<ListItemText primary={<img alt={data?.name} src={data.image || theme.palette.defaultImage} style={imageStyle} />} style={{ width: 100,
minWidth: 100,
maxWidth: 100,
display: "table-cell",
padding: "8px 8px 8px 20px",
textAlign: "center", }} />
<ListItemText primary={data.name} style={{ minWidth: 100,
maxWidth: 300,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
display: "table-cell",
padding: 8,
verticalAlign: "middle",
textAlign: "center", }} />
{isCloud && (
<ListItemText
primary={
<div style={{ display: "flex", alignItems: "center" }}>
<img
alt={regiontag}
src={`https://flagcdn.com/w20/${regionCode}.png`}
style={{ width: "30px", height: "20px", marginRight: "5px" }}
/>
<ListItemText primary={regiontag?.toUpperCase()} />
</div>
}
style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }}
/>
)}
<ListItemText primary={data.id} style={{ minWidth: 300,
maxWidth: 300,
display: "table-cell",
padding: 8,
verticalAlign: "middle", }} />
{isCloud && (
<ListItemText
primary={
<div style={{ display: "flex", alignItems: "center" }}>
<img
alt={regiontag}
src={`https://flagcdn.com/w20/${regionCode}.png`}
style={{ width: "30px", height: "20px", marginRight: "5px" }}
/>
<ListItemText primary={regiontag?.toUpperCase()} />
</div>
}
style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }}
/>
)}
<ListItemText primary={data.id} style={{ minWidth: 300,
maxWidth: 300,
display: "table-cell",
padding: 8,
verticalAlign: "middle", }} />
<ListItemText
primary={
<Tooltip title={data.id === userdata?.active_org?.id ? "You are already in this organization." : ""} disableInteractive>
<Button
color="primary"
variant='outlined'
disabled={data.id === userdata?.active_org?.id}
onClick={() => {
handleClickChangeOrg(data.id);
}}
sx={{
boxShadow:"none", textTransform:"capitalize",
fontSize:16,
'&.Mui-disabled': {
backgroundColor: 'rgba(200, 200, 200, 0.5)',
color: '#A9A9A9',
},
}}
>
Change Active Org
</Button>
</Tooltip>
}
style={{ display: "table-cell", verticalAlign: "middle" }}
/>
</ListItem>
)})}
</span>}
<ListItemText
primary={
<Tooltip title={data.id === userdata?.active_org?.id ? "You are already in this organization." : ""} disableInteractive>
<Button
color="primary"
variant='outlined'
disabled={data.id === userdata?.active_org?.id}
onClick={() => {
handleClickChangeOrg(data.id);
}}
sx={{
boxShadow:"none", textTransform:"capitalize",
fontSize:16,
'&.Mui-disabled': {
backgroundColor: 'rgba(200, 200, 200, 0.5)',
color: '#A9A9A9',
},
}}
>
Change Active Org
</Button>
</Tooltip>
}
style={{ display: "table-cell", verticalAlign: "middle" }}
/>
</ListItem>
)})}
</List>
</div>
</div>
@@ -1363,247 +1414,289 @@ const TenantsTab = memo((props) => {
paddingBottom: 0,
}}
>
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
}}
>
<ListItemText
primary="Logo"
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Name"
style={{
minWidth: 100,
maxWidth: 300,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
textAlign: "center",
}}
/>
{isCloud && (
<ListItemText
primary="Region"
style={{
minWidth: 100,
maxWidth: 100,
display: "table-cell",
borderBottom: "1px solid #494949",
padding: "0px 8px 8px 8px",
}}
/>
)}
<ListItemText
primary="id"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Action"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
{!allTenantsOpen ?
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
itemAlign: "center",
{userdata?.orgs?.length <= 0 ? (
[...Array(6)].map((_, rowIndex) => (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: "#212121",
}}
>
{Array(7)
.fill()
.map((_, colIndex) => (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: "#1a1a1a",
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
))}
</ListItem>
))
) : (
userdata?.orgs?.length > 0 &&
userdata.orgs.map((data, index) => {
let regiontag = "UK";
let regionCode = "gb";
if (data.region_url?.length > 0) {
const regionsplit = data.region_url.split(".");
if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) {
const namesplit = regionsplit[0].split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
}}
>
<ListItemText
primary={
<Button
fullWidth
variant="secondary"
style={{
textTransform: 'none',
}}
onClick={() => setAllTenantsOpen(true)}
>
Show ALL your tenants <ArrowDropDownIcon />
</Button>
}
}
}
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
:
<span>
<ListItem
style={{
width: "100%",
display: "table-row",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
verticalAlign: "middle",
}}
>
<ListItemText
primary="Logo"
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
paddingLeft: 20,
display: "table-cell",
padding: "0px 8px 8px 8px",
textAlign: "center",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Name"
style={{
minWidth: 100,
maxWidth: 300,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
textAlign: "center",
}}
/>
{isCloud && (
<ListItemText
primary="Region"
style={{
minWidth: 100,
maxWidth: 100,
display: "table-cell",
borderBottom: "1px solid #494949",
padding: "0px 8px 8px 8px",
}}
/>
)}
<ListItemText
primary="id"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
<ListItemText
primary="Action"
style={{
minWidth: 400,
maxWidth: 400,
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949",
verticalAlign: "middle",
}}
/>
</ListItem>
return (
<ListItem
key={index}
style={{
display: "table-row",
verticalAlign: "middle",
padding: 8,
backgroundColor: index % 2 === 0 ? "#1A1A1A" : "#212121",
borderBottomLeftRadius:
userdata?.orgs?.length - 1 === index ? 8 : 0,
borderBottomRightRadius:
userdata?.orgs?.length - 1 === index ? 8 : 0,
}}
>
<ListItemText
primary={
<img
alt={data.name}
src={data.image || theme.palette.defaultImage}
style={imageStyle}
/>
}
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
display: "table-cell",
padding: "8px 8px 8px 20px",
textAlign: "center",
}}
/>
<ListItemText
primary={data?.name}
style={{
minWidth: 100,
maxWidth: 300,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
display: "table-cell",
padding: 8,
verticalAlign: "middle",
textAlign: "center",
}}
></ListItemText>
{isCloud ? (
<ListItemText
primary={
<div style={{ display: "flex", alignItems: "center" }}>
<img
alt={regiontag}
src={`https://flagcdn.com/w20/${regionCode}.png`}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
/>
{userdata?.orgs?.length <= 0 ? (
[...Array(6)].map((_, rowIndex) => (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: "#212121",
}}
>
{Array(7)
.fill()
.map((_, colIndex) => (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: "#1a1a1a",
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
))}
</ListItem>
))
) : (
userdata?.orgs?.length > 0 &&
userdata.orgs.map((data, index) => {
let regiontag = "UK";
let regionCode = "gb";
<ListItemText primary={regiontag} />
</div>
}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
></ListItemText>
) : null}
<ListItemText
primary={data.id}
style={{
minWidth: 300,
maxWidth: 300,
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
/>
<ListItemText
primary={
<Button
variant="outlined"
style={{
whiteSpace: "nowrap",
textTransform: "none",
fontSize: 16,
}}
disabled={data?.id === userdata?.active_org?.id}
onClick={() => {
handleClickChangeOrg(data?.id);
}}
>
Change Active Org
</Button>
}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
></ListItemText>
</ListItem>
);
})
)}
if (data.region_url?.length > 0) {
const regionsplit = data.region_url.split(".");
if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) {
const namesplit = regionsplit[0].split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
}
}
}
return (
<ListItem
key={index}
style={{
display: "table-row",
verticalAlign: "middle",
padding: 8,
backgroundColor: index % 2 === 0 ? "#1A1A1A" : "#212121",
borderBottomLeftRadius:
userdata?.orgs?.length - 1 === index ? 8 : 0,
borderBottomRightRadius:
userdata?.orgs?.length - 1 === index ? 8 : 0,
}}
>
<ListItemText
primary={
<img
alt={data.name}
src={data.image || theme.palette.defaultImage}
style={imageStyle}
/>
}
style={{
width: 100,
minWidth: 100,
maxWidth: 100,
display: "table-cell",
padding: "8px 8px 8px 20px",
textAlign: "center",
}}
/>
<ListItemText
primary={data?.name}
style={{
minWidth: 100,
maxWidth: 300,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
display: "table-cell",
padding: 8,
verticalAlign: "middle",
textAlign: "center",
}}
></ListItemText>
{isCloud ? (
<ListItemText
primary={
<div style={{ display: "flex", alignItems: "center" }}>
<img
alt={regiontag}
src={`https://flagcdn.com/w20/${regionCode}.png`}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
/>
<ListItemText primary={regiontag} />
</div>
}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
></ListItemText>
) : null}
<ListItemText
primary={data.id}
style={{
minWidth: 300,
maxWidth: 300,
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
/>
<ListItemText
primary={
<Button
variant="outlined"
style={{
whiteSpace: "nowrap",
textTransform: "none",
fontSize: 16,
}}
disabled={data?.id === userdata?.active_org?.id}
onClick={() => {
handleClickChangeOrg(data?.id);
}}
>
Change Active Org
</Button>
}
style={{
display: "table-cell",
padding: 8,
verticalAlign: "middle",
}}
></ListItemText>
</ListItem>
);
})
)}
</span>}
</List>
</div>
</div>
@@ -248,6 +248,14 @@ const UserManagmentTab = memo((props) => {
return;
}
if (event.target.value.includes("ALL")) {
toast.info("Adding to available all sub-organizations. This may take a minute.")
event.target.value = selectedOrganization.child_orgs.map((org) => org.id)
} else if (event.target.value.includes("None")) {
toast.info("Removing from all sub-organizations. This may take a minute")
event.target.value = []
}
console.log("event: ", event.target.value);
setMatchingOrganizations(event.target.value);
// Workaround for empty orgs
@@ -286,6 +294,14 @@ const UserManagmentTab = memo((props) => {
}}
MenuProps={MenuProps}
>
<MenuItem key={-2} value={"None"}>
<Checkbox checked={false} />
<ListItemText primary={"None"} />
</MenuItem>
<MenuItem key={-1} value={"ALL"}>
<Checkbox checked={false} />
<ListItemText primary={"ALL"} />
</MenuItem>
{selectedOrganization.child_orgs.map((org, index) => (
<MenuItem key={index} value={org.id}>
<Checkbox checked={matchingOrganizations.indexOf(org.id) > -1} />
@@ -608,8 +608,6 @@ const WorkflowValidationTimeline = (props) => {
const ballsize = 8
const topMargin = 20
console.log("CHIP: ", index, chipColor, chipBackground)
const chipStyle = {
height: 40,
minWidth: 125,
+39 -6
View File
@@ -95,23 +95,56 @@ const data = [
{
selector: `node[type="COMMENT"]`,
css: {
label: function(element) {
return element.data("label")
},
label: function (element) {
return element.data("label")
},
shape: "roundrectangle",
color: "data(color)",
width: "data(width)",
height: "data(height)",
padding: "0px",
padding: "5px",
margin: "0px",
"background-color": "data(backgroundcolor)",
"background-image": "data(backgroundimage)",
"border-color": "#ffffff",
"text-margin-x": "0px",
"text-margin-x": "data(textMarginX)",
"text-margin-y": "data(textMarginY)",
"z-index": 4999,
"border-radius": "5px",
"background-opacity": "0.5",
"text-wrap": "wrap",
"text-wrap": "wrap",
"text-max-width": "data(width)",
"text-halign": function(element) {
const align = element?.data("textHalign")
if (align === null || align === undefined || align === "") {
return "center"
}
return align
},
"text-valign": function(element) {
const align = element?.data("textValign")
if (align === null || align === undefined || align === "") {
return "center"
}
return align
}
},
},
{
selector: `node[type="RESIZE-HANDLE"]`,
css: {
shape: "ellipse",
width: "8px",
height: "8px",
"border-width": 1,
"border-color": "white",
"z-index": 5002,
"overlay-opacity": 0,
"cursor": "nwse-resize",
"opacity": 0,
"pointer-events": "auto",
},
},
{
+11 -11
View File
@@ -27,7 +27,7 @@ const theme = createTheme(adaptV4Theme({
distributionColor: "#40E0D0",
green: "#5cc879",
borderRadius: 10,
borderRadius: 8,
defaultBorder: "1px solid rgba(255,255,255,0.3)",
//jsonTheme: "brewer",
@@ -112,39 +112,39 @@ const theme = createTheme(adaptV4Theme({
MuiCssBaseline: {
styleOverrides: `
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light');
src: local('Inter Light'), local('Inter-Light');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular');
src: local('Inter Regular'), local('Inter-Regular');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium');
src: local('Inter Medium'), local('Inter-Medium');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
src: local('Roboto SemiBold'), local('Roboto-SemiBold');
src: local('Inter SemiBold'), local('Inter-SemiBold');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold');
src: local('Inter Bold'), local('Inter-Bold');
}
`,
},
+12
View File
@@ -76,6 +76,18 @@ const Admin2 = (props) => {
leads.push("tech partner");
}
if (responseJson.lead_info.integration_partner) {
leads.push("integration partner");
}
if (responseJson.lead_info.distribution_partner) {
leads.push("distribution partner");
}
if (responseJson.lead_info.service_partner) {
leads.push("service partner");
}
if (responseJson.lead_info.creator) {
leads.push("creator");
}
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -943,8 +943,8 @@ const AppCreator = (defaultprops) => {
if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) {
//const regex = /_shuffle_replace_\d/i;
const regex = /_shuffle_replace_\d+/i
newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
const newurl = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
newaction.url = newurl
}
// Finding category
@@ -956,7 +956,6 @@ const AppCreator = (defaultprops) => {
if (pathsplit[splitkey].includes("_shuffle_replace_")) {
//const regex = /_shuffle_replace_\d/i;
const regex = /_shuffle_replace_\d+/i
//console.log("NEW: ",
pathsplit[splitkey] = pathsplit[splitkey].replaceAll(new RegExp(regex, 'g'), "")
}
@@ -2037,7 +2036,7 @@ const AppCreator = (defaultprops) => {
for (let actionkey in actions) {
var item = JSON.parse(JSON.stringify(actions[actionkey]))
if (item.errors.length > 0) {
toast("Saving with error in action " + item.name);
//toast("Saving with error in action " + item.name);
}
if (item.name === undefined && item.description !== undefined) {
@@ -3858,7 +3857,7 @@ const AppCreator = (defaultprops) => {
if (currentAction.url === "" && actions !== undefined && actions !== null && actions.length > 0) {
for (var i = 0; i < actions.length; i++) {
if (actions[i].name.toLowerCase() === e.target.value.toLowerCase()) {
toast("Action with name " + e.target.value + " already exists. If you keep this, it will be overwritten.")
//toast("Action with name " + e.target.value + " already exists. If you keep this, it will be overwritten.")
break
}
}
+4 -5
View File
@@ -1088,14 +1088,13 @@ const AppExplorer = (props) => {
setNewWorkflowTags(newWorkflowTags);
}
// This is annoying (:
var securitySchemes = data.components.securityDefinitions;
var securitySchemes = data?.components?.securityDefinitions;
if (securitySchemes === undefined) {
securitySchemes = data.securitySchemes;
securitySchemes = data?.securitySchemes;
}
if (securitySchemes === undefined) {
securitySchemes = data.components.securitySchemes;
securitySchemes = data?.components?.securitySchemes;
}
const allowedfunctions = [
@@ -1681,7 +1680,7 @@ const AppExplorer = (props) => {
setExecutionResult({
valid: false,
result:
"Couldn't finish execution (2). Please fill all the required fields, and validate the execution.",
"Couldn't finish execution OR no result was returned (2). Please fill all the required fields, and validate the execution.",
});
}
+13 -5
View File
@@ -343,10 +343,15 @@ const Docs = (defaultprops) => {
return
}
console.log("PROPKEY: ", propkey)
if (location.pathname.includes("/docs/")) {
if (propkey === "cookie_policy" || propkey === "compliance" || propkey === "privacy_policy" || propkey === "terms_of_service") {
navigate(`/legal/${propkey}`)
}
if (propkey === "app_creation") {
navigate('/docs/apps#app-creation-introduction')
}
}
}, [location]);
@@ -932,7 +937,7 @@ const Docs = (defaultprops) => {
}
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found") && !isArticlePage) {
navigate("/docs")
//navigate("/docs")
return
}
@@ -1109,6 +1114,7 @@ const Docs = (defaultprops) => {
marginTop: 25,
}
const showPartnerLogo = userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.image !== undefined && userdata?.active_org?.image !== null && userdata?.active_org?.image.length > 0
const mainpageInfo =
<div style={{
color: "rgba(255, 255, 255, 0.65)",
@@ -1124,10 +1130,12 @@ const Docs = (defaultprops) => {
<Typography variant="h4" style={{ textAlign: "center", marginTop: 20 }}>
Documentation
</Typography>
<div style={{ display: "flex", marginTop: 25, }}>
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> />
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
</div>
{showPartnerLogo === true ? null :
<div style={{ display: "flex", marginTop: 25, }}>
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> />
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
</div>
}
<div style={{ textAlign: "left" }}>
<Typography variant="h6" style={headerStyle} >Tutorial</Typography>
+11 -3
View File
@@ -323,9 +323,17 @@ const LoginPage = props => {
if (document !== undefined) {
if (register) {
document.title = "Login to Shuffle SaaS"
if (isCloud) {
document.title = "Login to Shuffle SaaS"
} else {
document.title = "Login to Shuffle"
}
} else {
document.title = "Register to Shuffle SaaS"
if (isCloud) {
document.title = "Register to Shuffle SaaS"
} else {
document.title = "Register to Shuffle"
}
}
}
@@ -375,7 +383,7 @@ const LoginPage = props => {
padding: "40px",
flex: 1,
maxWidth: isMobile ? "100%" : 410,
minWidth: 410,
minWidth: isCloud ? 410 : 475,
background: "#212121",
borderRadius: "12px",
display: "flex",
+194 -24
View File
@@ -19,6 +19,7 @@ import RecentWorkflow from "../components/RecentWorkflow.jsx";
import {
Tooltip,
Fade,
Select,
IconButton,
CircularProgress,
@@ -33,6 +34,7 @@ import {
DialogTitle,
DialogContent,
MenuItem,
Autocomplete,
} from '@mui/material';
import {
@@ -44,6 +46,7 @@ import {
LockOpen as LockOpenIcon,
OpenInNew as OpenInNewIcon,
Edit as EditIcon,
Polyline as PolylineIcon,
} from '@mui/icons-material';
const hrefStyle = {
@@ -74,6 +77,7 @@ const RunWorkflow = (defaultprops) => {
const [sharingOpen, setSharingOpen] = React.useState(false)
const [realtimeMarkdown, setRealtimeMarkdown] = React.useState("")
const [forms, setForms] = React.useState([])
const [workflows, setWorkflows] = React.useState([])
const [boxWidth, setBoxWidth] = React.useState(500)
const [inputQuestions, setInputQuestions] = React.useState([])
@@ -180,6 +184,37 @@ const RunWorkflow = (defaultprops) => {
return true
}
const getWorkflows = () => {
const url = `${globalUrl}/api/v1/workflows`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for org forms");
}
return response.json()
})
.then((responseJson) => {
if (responseJson.success === false) {
toast.error("Failed saving workflow. Please try again.")
} else {
if (responseJson?.length > 0) {
setWorkflows(responseJson)
}
}
})
.catch((error) => {
//toast.error("Load form error: " + error)
})
}
const loadForms = (orgId) => {
const url = `${globalUrl}/api/v1/orgs/${orgId}/forms`
fetch(url, {
@@ -462,6 +497,8 @@ const RunWorkflow = (defaultprops) => {
} else {
console.log("Started execution")
start()
setExecutionRunning(true);
if (answer !== undefined && answer !== null) {
console.log("Skipping start")
} else {
@@ -875,6 +912,7 @@ const RunWorkflow = (defaultprops) => {
// Just use this one?
var url = execution_id !== undefined && authorization !== undefined ? `${globalUrl}/api/v1/orgs/${orgId}?reference_execution=${execution_id}&authorization=${authorization}` : `${globalUrl}/api/v1/orgs/${orgId}`;
getWorkflows()
loadForms(orgId)
fetch(url, {
@@ -1055,7 +1093,7 @@ const RunWorkflow = (defaultprops) => {
if (parsedresult.click_info !== undefined && parsedresult.click_info !== null) {
if (parsedresult.click_info.user !== undefined && parsedresult.click_info.user !== null && parsedresult.click_info.user.length > 0) {
setMessage("Already answered by " + parsedresult.click_info.user)
setMessage("Answered by " + parsedresult.click_info.user)
}
} else {
@@ -1149,10 +1187,100 @@ const RunWorkflow = (defaultprops) => {
No Forms Found
</Typography>
<Typography variant="body1" color="textSecondary">
<b>Every</b> Workflow is a form, and can be accessed by going to /forms/{`{workflow_id}`}. You can control the form by editing the workflow details in the "Forms" section.
</Typography>
<b>ALL</b> Workflows are forms, and can be accessed by going to /forms/{`{workflow_id}`}. You can control the form by editing the workflow details in the "Forms" section.
</Typography>
</div>
}
{workflows === undefined || workflows === null || workflows.length === 0 ? null :
<Autocomplete
disabled={workflows === undefined || workflows === null || workflows.length === 0}
id="form-workflow-search"
autoHighlight
value={""}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: "white",
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40, // Adjust the input height
},
'& .MuiAutocomplete-input': {
padding: '8px', // Adjust the text padding
},
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.name === undefined ||
option.name === null
) {
return "No Workflow Selected";
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={workflows}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
marginTop: 75,
}}
renderOption={(props, data, state) => {
if (data.id === workflow.id) {
data = workflow;
}
//key={index}
return (
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, maxWidth: 285, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose Subflow '{data.name}'
</Typography>
</span>
}>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
}}
onClick={() => {
window.location.href = `/forms/${data.id}`
}}
value={data}
>
<PolylineIcon style={{ marginRight: 8 }} />
{data.name}
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<div style={{ display: "flex", }}>
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Find your form"
variant="outlined"
/>
</div>
)
}}
/>
}
</div>
)
}
@@ -1325,7 +1453,8 @@ const RunWorkflow = (defaultprops) => {
})}
</div>
:
answer !== undefined && answer !== null ? null :
(answer !== undefined && answer !== null) || message !== "" ? null :
<span>
Runtime Argument
<div style={{marginBottom: 5}}>
@@ -1334,7 +1463,13 @@ const RunWorkflow = (defaultprops) => {
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
multiLine
maxRows={2}
type="text"
autoComplete="off"
InputProps={{
autocomplete: "off",
form: {
autocomplete: "off",
},
style:{
height: "50px",
color: "white",
@@ -1375,9 +1510,11 @@ const RunWorkflow = (defaultprops) => {
{message}. You may close this window.
</Typography>
:
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
{disabledButtons ? "Answered. You may close this window." : ""}
</Typography>
<Fade in={true} timeout={2500}>
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
{disabledButtons ? "Answered. You may close this window." : ""}
</Typography>
</Fade>
}
{disabledButtons ? null :
@@ -1387,25 +1524,47 @@ const RunWorkflow = (defaultprops) => {
}
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
<Button fullWidth id="continue_execution" variant="contained" disabled={!handleValidateForm(executionArgument) || disabledButtons} color="primary" style={{flex: 1,}} onClick={() => {
setButtonClicked("FINISHED")
setExecutionData({
status: "FINISHED",
})
<Button
fullWidth
id="continue_execution"
variant="contained"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}}
onClick={() => {
setButtonClicked("FINISHED")
setExecutionData({
status: "FINISHED",
})
onSubmit(null, execution_id, authorization, true)
}}>Continue</Button>
onSubmit(null, execution_id, authorization, true)
}}>
Continue</Button>
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
&nbsp;or&nbsp;
</Typography>
<Button fullWidth id="abort_execution" variant="contained" disabled={!handleValidateForm(executionArgument) || disabledButtons} color="primary" style={{ flex: 1, }} onClick={() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
<Button
fullWidth
id="abort_execution"
variant="outlined"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}} onClick={() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
onSubmit(null, execution_id, authorization, false)
}}>Stop</Button>
onSubmit(null, execution_id, authorization, false)
}}>
Stop
</Button>
</div>
</span>
:
@@ -1416,6 +1575,9 @@ const RunWorkflow = (defaultprops) => {
color="primary"
fullWidth
disabled={!handleValidateForm(executionArgument) || executionLoading}
style={{
textTransform: "none",
}}
>
{executionLoading ?
<CircularProgress color="secondary" style={{color: "white",}} />
@@ -1601,7 +1763,10 @@ const RunWorkflow = (defaultprops) => {
disabled={workflow.id === undefined || workflow.id === null}
variant={"outlined"}
color={"secondary"}
style={{marginRight: 10, }}
style={{
marginRight: 10,
textTransform: "none",
}}
onClick={() => {
window.open(`/workflows/${workflow.id}`, "_blank")
}}
@@ -1614,7 +1779,10 @@ const RunWorkflow = (defaultprops) => {
disabled={workflow.id === undefined || workflow.id === null}
variant={workflow.sharing === "form" ? "outlined" : "contained"}
color={"secondary"}
style={{marginRight: 10, }}
style={{
marginRight: 10,
textTransform: "none",
}}
onClick={() => {
setSharingOpen(true)
}}
@@ -1636,7 +1804,9 @@ const RunWorkflow = (defaultprops) => {
disabled={workflow.id === undefined || workflow.id === null}
variant={"contained"}
color={"primary"}
style={{}}
style={{
textTransform: "none",
}}
onClick={() => {
setEditWorkflowModalOpen(true)
}}
+9 -2
View File
@@ -527,6 +527,8 @@ const Settings = (props) => {
};
const generateApikey = () => {
toast.info("Generating new API key. This may take a bit.");
fetch(globalUrl + "/api/v1/generateapikey", {
method: "GET",
headers: {
@@ -538,7 +540,7 @@ const Settings = (props) => {
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
}
return response.json();
})
@@ -836,7 +838,12 @@ const Settings = (props) => {
variant="outlined"
/>
<Button
style={{ width: "100%", height: "40px", marginTop: "10px" }}
style={{
width: "100%",
height: "40px",
marginTop: "10px",
textTransform: "none",
}}
variant="outlined"
color="primary"
onClick={() => generateApikey()}
+44 -67
View File
@@ -661,7 +661,7 @@ const Workflows2 = (props) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false);
const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false);
const [view, setView] = useState("grid");
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const classes = useStyles(theme)
const imgSize = 60;
@@ -812,9 +812,10 @@ const Workflows2 = (props) => {
}
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
//const isCloud =
// window.location.host === "localhost:3002" ||
// window.location.host === "shuffler.io";
const isCloud = false
const findWorkflow = (filters) => {
console.log("Using filters: ", filters)
@@ -2074,55 +2075,6 @@ const Workflows2 = (props) => {
};
const hasWorkflows = workflows === undefined || workflows === null || workflows.length === 0
const NewWorkflowPaper = () => {
const [hover, setHover] = React.useState(false);
const innerColor = "rgba(255,255,255,0.3)"
const setupPaperStyle = {
minHeight: paperAppStyle.minHeight,
maxWidth: "100%",
minWidth: paperAppStyle.width,
color: innerColor,
padding: paperAppStyle.padding,
display: "flex",
boxSizing: "border-box",
position: "relative",
border: hasWorkflows ? `2px solid #f85a3e` : `2px solid ${innerColor}`,
cursor: "pointer",
backgroundColor: hover ? "rgba(39,41,45,0.5)" : "rgba(39,41,45,1)",
borderRadius: paperAppStyle.borderRadius,
}
return (
<Grid item xs={isMobile ? 12 : hasWorkflows ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
<Paper
square
style={setupPaperStyle}
onClick={() => {
setModalOpen(true)
setIsEditing(false)
}}
onMouseOver={() => {
setHover(true);
}}
onMouseOut={() => {
setHover(false);
}}
>
<Tooltip title={`New Workflow`} placement="bottom">
<span style={{ textAlign: "center", minWidth: 240, margin: "auto" }}>
<AddCircleIcon style={{ height: 65, width: 65 }} />
<Typography variant="h6" style={{ color: innerColor, margin: "auto" }}>
New Workflow
</Typography>
</span>
</Tooltip>
</Paper>
</Grid>
);
};
const getWorkflowAppgroup = (data) => {
if (currTab !== 2) {
if (data.actions === undefined || data.actions === null) {
@@ -2517,21 +2469,24 @@ const Workflows2 = (props) => {
/>
</Tooltip>
: null}
<Grid
item
style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme?.typography?.fontFamily }}
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
<Tooltip title={`Org "${orgName}". Click to edit image.`} placement="bottom">
<div
styl={{ cursor: "pointer" }}
onClick={() => {
navigate("/admin")
}}
>
{image}
</div>
</Tooltip>
{currTab === 2 ? null :
<Tooltip title={`Org "${orgName}". Click to edit image.`} placement="bottom">
<div
styl={{ cursor: "pointer" }}
onClick={() => {
navigate("/admin")
}}
>
{image}
</div>
</Tooltip>
}
<Tooltip arrow
onMouseEnter={() => {
/*
@@ -2610,10 +2565,19 @@ const Workflows2 = (props) => {
>
<Link
to={
type === "public" ? parsedUrl : data.workflow_as_code ? `/workflows/${data.id}/code` : `/workflows/${data.id}`
currTab === 2 ? `https://shuffler.io${parsedUrl}` : type === "public" ? parsedUrl : data.workflow_as_code ? `/workflows/${data.id}/code` : `/workflows/${data.id}`
}
style={{ textDecoration: "none", color: "inherit" }}
>
style={{
textDecoration: "none",
color: "inherit",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: "90%",
display: "block"
}}
target={currTab === 2 ? "_blank" : "_self"}
>
{parsedName}
</Link>
</Typography>
@@ -4310,7 +4274,7 @@ const Workflows2 = (props) => {
TabIndicatorProps={{ style: { display: 'none' } }}
>
<Tab
label="Organization Workflows"
label="Org Workflows"
style={{
...tabStyle,
...(currTab === 0 ? tabActive : {})
@@ -4331,6 +4295,19 @@ const Workflows2 = (props) => {
...(currTab === 2 ? tabActive : {})
}}
/>
<Tab
label="Org Forms"
onClick={() => {
navigate("/forms")
}}
style={{
...tabStyle,
marginRight: 0,
marginLeft: 25,
...(currTab === 3 ? tabActive : {})
}}
/>
</Tabs>
</div>
+2 -2
View File
@@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
version: 0.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
appVersion: "2.0.0"
@@ -2,8 +2,8 @@ apiVersion: v2
name: shuffle
description: A Helm chart for deploying Shuffle on Kubernetes
type: application
version: 0.0.0
appVersion: 0.0.0
version: 0.0.0 # Set during publishing in GitHub actions
appVersion: nightly # Overwritten during publishing in GitHub actions
dependencies:
- name: common
version: ^2.23.0
+12 -18
View File
@@ -148,6 +148,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `backend.image.registry` | backend image registry | `ghcr.io` |
| `backend.image.repository` | backend image repository | `shuffle/shuffle-backend` |
| `backend.image.tag` | backend image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `backend.image.digest` | backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `backend.image.pullPolicy` | backend image pull policy | `IfNotPresent` |
| `backend.image.pullSecrets` | backend image pull secrets | `[]` |
@@ -184,8 +185,8 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `backend.podSecurityContext.fsGroup` | Set fsGroup in backend pods' Security Context | `1001` |
| `backend.containerSecurityContext.enabled` | Enabled backend container' Security Context | `true` |
| `backend.containerSecurityContext.seLinuxOptions` | Set SELinux options in backend container | `{}` |
| `backend.containerSecurityContext.runAsUser` | Set runAsUser in backend container' Security Context | `1000` |
| `backend.containerSecurityContext.runAsGroup` | Set runAsGroup in backend container' Security Context | `1000` |
| `backend.containerSecurityContext.runAsUser` | Set runAsUser in backend container' Security Context | `1001` |
| `backend.containerSecurityContext.runAsGroup` | Set runAsGroup in backend container' Security Context | `1001` |
| `backend.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in backend container' Security Context | `true` |
| `backend.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in backend container' Security Context | `true` |
| `backend.containerSecurityContext.privileged` | Set privileged in backend container' Security Context | `false` |
@@ -196,9 +197,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `backend.args` | Override default backend container args (useful when using custom images) | `[]` |
| `backend.automountServiceAccountToken` | Mount Service Account token in backend pods | `true` |
| `backend.hostAliases` | backend pods host aliases | `[]` |
| `backend.daemonsetAnnotations` | Annotations for backend daemonset | `{}` |
| `backend.deploymentAnnotations` | Annotations for backend deployment | `{}` |
| `backend.statefulsetAnnotations` | Annotations for backend statefulset | `{}` |
| `backend.podLabels` | Extra labels for backend pods | `{}` |
| `backend.podAnnotations` | Annotations for backend pods | `{}` |
| `backend.podAffinityPreset` | Pod affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `""` |
@@ -210,8 +209,6 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `backend.nodeSelector` | Node labels for backend pods assignment | `{}` |
| `backend.tolerations` | Tolerations for backend pods assignment | `[]` |
| `backend.updateStrategy.type` | backend deployment strategy type | `RollingUpdate` |
| `backend.updateStrategy.type` | backend statefulset strategy type | `RollingUpdate` |
| `backend.podManagementPolicy` | Pod management policy for backend statefulset | `OrderedReady` |
| `backend.priorityClassName` | backend pods' priorityClassName | `""` |
| `backend.topologySpreadConstraints` | Topology Spread Constraints for backend pod assignment spread across your cluster among failure-domains | `[]` |
| `backend.schedulerName` | Name of the k8s scheduler (other than default) for backend pods | `""` |
@@ -265,6 +262,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `frontend.image.registry` | frontend image registry | `ghcr.io` |
| `frontend.image.repository` | frontend image repository | `shuffle/shuffle-frontend` |
| `frontend.image.tag` | frontend image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `frontend.image.digest` | frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `frontend.image.pullPolicy` | frontend image pull policy | `IfNotPresent` |
| `frontend.image.pullSecrets` | frontend image pull secrets | `[]` |
@@ -302,8 +300,8 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `frontend.podSecurityContext.fsGroup` | Set fsGroup in frontend pods' Security Context | `1001` |
| `frontend.containerSecurityContext.enabled` | Enabled frontend container' Security Context | `false` |
| `frontend.containerSecurityContext.seLinuxOptions` | Set SELinux options in frontend container | `{}` |
| `frontend.containerSecurityContext.runAsUser` | Set runAsUser in frontend container' Security Context | `101` |
| `frontend.containerSecurityContext.runAsGroup` | Set runAsGroup in frontend container' Security Context | `101` |
| `frontend.containerSecurityContext.runAsUser` | Set runAsUser in frontend container' Security Context | `1001` |
| `frontend.containerSecurityContext.runAsGroup` | Set runAsGroup in frontend container' Security Context | `1001` |
| `frontend.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in frontend container' Security Context | `true` |
| `frontend.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in frontend container' Security Context | `true` |
| `frontend.containerSecurityContext.privileged` | Set privileged in frontend container' Security Context | `false` |
@@ -314,9 +312,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `frontend.args` | Override default frontend container args (useful when using custom images) | `[]` |
| `frontend.automountServiceAccountToken` | Mount Service Account token in frontend pods | `false` |
| `frontend.hostAliases` | frontend pods host aliases | `[]` |
| `frontend.daemonsetAnnotations` | Annotations for frontend daemonset | `{}` |
| `frontend.deploymentAnnotations` | Annotations for frontend deployment | `{}` |
| `frontend.statefulsetAnnotations` | Annotations for frontend statefulset | `{}` |
| `frontend.podLabels` | Extra labels for frontend pods | `{}` |
| `frontend.podAnnotations` | Annotations for frontend pods | `{}` |
| `frontend.podAffinityPreset` | Pod affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `""` |
@@ -328,8 +324,6 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `frontend.nodeSelector` | Node labels for frontend pods assignment | `{}` |
| `frontend.tolerations` | Tolerations for frontend pods assignment | `[]` |
| `frontend.updateStrategy.type` | frontend deployment strategy type | `RollingUpdate` |
| `frontend.updateStrategy.type` | frontend statefulset strategy type | `RollingUpdate` |
| `frontend.podManagementPolicy` | Pod management policy for frontend statefulset | `OrderedReady` |
| `frontend.priorityClassName` | frontend pods' priorityClassName | `""` |
| `frontend.topologySpreadConstraints` | Topology Spread Constraints for frontend pod assignment spread across your cluster among failure-domains | `[]` |
| `frontend.schedulerName` | Name of the k8s scheduler (other than default) for frontend pods | `""` |
@@ -373,6 +367,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `orborus.image.registry` | orborus image registry | `ghcr.io` |
| `orborus.image.repository` | orborus image repository | `shuffle/shuffle-orborus` |
| `orborus.image.tag` | orborus image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `orborus.image.digest` | orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `orborus.image.pullPolicy` | orborus image pull policy | `IfNotPresent` |
| `orborus.image.pullSecrets` | orborus image pull secrets | `[]` |
@@ -409,8 +404,8 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `orborus.podSecurityContext.fsGroup` | Set fsGroup in orborus pods' Security Context | `1001` |
| `orborus.containerSecurityContext.enabled` | Enabled orborus container' Security Context | `true` |
| `orborus.containerSecurityContext.seLinuxOptions` | Set SELinux options in orborus container | `{}` |
| `orborus.containerSecurityContext.runAsUser` | Set runAsUser in orborus container' Security Context | `101` |
| `orborus.containerSecurityContext.runAsGroup` | Set runAsGroup in orborus container' Security Context | `101` |
| `orborus.containerSecurityContext.runAsUser` | Set runAsUser in orborus container' Security Context | `1001` |
| `orborus.containerSecurityContext.runAsGroup` | Set runAsGroup in orborus container' Security Context | `1001` |
| `orborus.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in orborus container' Security Context | `true` |
| `orborus.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in orborus container' Security Context | `true` |
| `orborus.containerSecurityContext.privileged` | Set privileged in orborus container' Security Context | `false` |
@@ -421,9 +416,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `orborus.args` | Override default orborus container args (useful when using custom images) | `[]` |
| `orborus.automountServiceAccountToken` | Mount Service Account token in orborus pods | `true` |
| `orborus.hostAliases` | orborus pods host aliases | `[]` |
| `orborus.daemonsetAnnotations` | Annotations for orborus daemonset | `{}` |
| `orborus.deploymentAnnotations` | Annotations for orborus deployment | `{}` |
| `orborus.statefulsetAnnotations` | Annotations for orborus statefulset | `{}` |
| `orborus.podLabels` | Extra labels for orborus pods | `{}` |
| `orborus.podAnnotations` | Annotations for orborus pods | `{}` |
| `orborus.podAffinityPreset` | Pod affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `""` |
@@ -435,8 +428,6 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `orborus.nodeSelector` | Node labels for orborus pods assignment | `{}` |
| `orborus.tolerations` | Tolerations for orborus pods assignment | `[]` |
| `orborus.updateStrategy.type` | orborus deployment strategy type | `RollingUpdate` |
| `orborus.updateStrategy.type` | orborus statefulset strategy type | `RollingUpdate` |
| `orborus.podManagementPolicy` | Pod management policy for orborus statefulset | `OrderedReady` |
| `orborus.priorityClassName` | orborus pods' priorityClassName | `""` |
| `orborus.topologySpreadConstraints` | Topology Spread Constraints for orborus pod assignment spread across your cluster among failure-domains | `[]` |
| `orborus.schedulerName` | Name of the k8s scheduler (other than default) for orborus pods | `""` |
@@ -481,6 +472,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `worker.image.registry` | worker image registry | `ghcr.io` |
| `worker.image.repository` | worker image repository | `shuffle/shuffle-worker` |
| `worker.image.tag` | worker image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `worker.image.digest` | worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `worker.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `worker.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
@@ -582,6 +574,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
| `volumePermissions.enabled` | Enable init container that changes the owner/group of the PV mount point to `runAsUser:fsGroup` | `false` |
| `volumePermissions.image.registry` | OS Shell + Utility image registry | `docker.io` |
| `volumePermissions.image.repository` | OS Shell + Utility image repository | `bitnami/os-shell` |
| `volumePermissions.image.tag` | OS Shell + Utility image tag (immutable tags are recommended) | `12-debian-12-r30` |
| `volumePermissions.image.pullPolicy` | OS Shell + Utility image pull policy | `IfNotPresent` |
| `volumePermissions.image.pullSecrets` | OS Shell + Utility image pull secrets | `[]` |
| `volumePermissions.resourcesPreset` | Set init container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if volumePermissions.resources is set (volumePermissions.resources is recommended for production). | `nano` |
@@ -605,3 +598,4 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
### Other Parameters
@@ -119,14 +119,14 @@ app.kubernetes.io/name: shuffle-app
Return the proper image name (for the init container volume-permissions image)
*/}}
{{- define "shuffle.volumePermissions.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.volumePermissions.image "global" .Values.global ) -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.volumePermissions.image "global" .Values.global "chart" .Chart ) -}}
{{- end -}}
{{/*
Return the proper Shuffle backend image name
*/}}
{{- define "shuffle.backend.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.backend.image "global" .Values.global ) -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.backend.image "global" .Values.global "chart" .Chart ) -}}
{{- end -}}
{{/*
@@ -140,7 +140,7 @@ Return the proper Docker Image Registry Secret Names for the backend pod
Return the proper Shuffle frontend image name
*/}}
{{- define "shuffle.frontend.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.frontend.image "global" .Values.global ) -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.frontend.image "global" .Values.global "chart" .Chart ) -}}
{{- end -}}
{{/*
@@ -154,7 +154,7 @@ Return the proper Docker Image Registry Secret Names for the frontend pod
Return the proper Shuffle orborus image name
*/}}
{{- define "shuffle.orborus.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.orborus.image "global" .Values.global ) -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.orborus.image "global" .Values.global "chart" .Chart ) -}}
{{- end -}}
{{/*
@@ -168,7 +168,7 @@ Return the proper Docker Image Registry Secret Names for the orborus pod
Return the proper Shuffle worker image name
*/}}
{{- define "shuffle.worker.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.worker.image "global" .Values.global ) -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.worker.image "global" .Values.global "chart" .Chart ) -}}
{{- end -}}
{{/*
@@ -155,6 +155,11 @@
"description": "backend image repository",
"default": "shuffle/shuffle-backend"
},
"tag": {
"type": "string",
"description": "backend image tag (immutable tags are recommended, defaults to appVersion)",
"default": ""
},
"digest": {
"type": "string",
"description": "backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)",
@@ -367,12 +372,12 @@
"runAsUser": {
"type": "number",
"description": "Set runAsUser in backend container' Security Context",
"default": 1000
"default": 1001
},
"runAsGroup": {
"type": "number",
"description": "Set runAsGroup in backend container' Security Context",
"default": 1000
"default": 1001
},
"runAsNonRoot": {
"type": "boolean",
@@ -444,21 +449,11 @@
"default": [],
"items": {}
},
"daemonsetAnnotations": {
"type": "object",
"description": "Annotations for backend daemonset",
"default": {}
},
"deploymentAnnotations": {
"type": "object",
"description": "Annotations for backend deployment",
"default": {}
},
"statefulsetAnnotations": {
"type": "object",
"description": "Annotations for backend statefulset",
"default": {}
},
"podLabels": {
"type": "object",
"description": "Extra labels for backend pods",
@@ -521,16 +516,11 @@
"properties": {
"type": {
"type": "string",
"description": "backend statefulset strategy type",
"description": "backend deployment strategy type",
"default": "RollingUpdate"
}
}
},
"podManagementPolicy": {
"type": "string",
"description": "Pod management policy for backend statefulset",
"default": "OrderedReady"
},
"priorityClassName": {
"type": "string",
"description": "backend pods' priorityClassName",
@@ -839,6 +829,11 @@
"description": "frontend image repository",
"default": "shuffle/shuffle-frontend"
},
"tag": {
"type": "string",
"description": "frontend image tag (immutable tags are recommended, defaults to appVersion)",
"default": ""
},
"digest": {
"type": "string",
"description": "frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)",
@@ -1056,12 +1051,12 @@
"runAsUser": {
"type": "number",
"description": "Set runAsUser in frontend container' Security Context",
"default": 101
"default": 1001
},
"runAsGroup": {
"type": "number",
"description": "Set runAsGroup in frontend container' Security Context",
"default": 101
"default": 1001
},
"runAsNonRoot": {
"type": "boolean",
@@ -1133,21 +1128,11 @@
"default": [],
"items": {}
},
"daemonsetAnnotations": {
"type": "object",
"description": "Annotations for frontend daemonset",
"default": {}
},
"deploymentAnnotations": {
"type": "object",
"description": "Annotations for frontend deployment",
"default": {}
},
"statefulsetAnnotations": {
"type": "object",
"description": "Annotations for frontend statefulset",
"default": {}
},
"podLabels": {
"type": "object",
"description": "Extra labels for frontend pods",
@@ -1210,16 +1195,11 @@
"properties": {
"type": {
"type": "string",
"description": "frontend statefulset strategy type",
"description": "frontend deployment strategy type",
"default": "RollingUpdate"
}
}
},
"podManagementPolicy": {
"type": "string",
"description": "Pod management policy for frontend statefulset",
"default": "OrderedReady"
},
"priorityClassName": {
"type": "string",
"description": "frontend pods' priorityClassName",
@@ -1463,6 +1443,11 @@
"description": "orborus image repository",
"default": "shuffle/shuffle-orborus"
},
"tag": {
"type": "string",
"description": "orborus image tag (immutable tags are recommended, defaults to appVersion)",
"default": ""
},
"digest": {
"type": "string",
"description": "orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)",
@@ -1675,12 +1660,12 @@
"runAsUser": {
"type": "number",
"description": "Set runAsUser in orborus container' Security Context",
"default": 101
"default": 1001
},
"runAsGroup": {
"type": "number",
"description": "Set runAsGroup in orborus container' Security Context",
"default": 101
"default": 1001
},
"runAsNonRoot": {
"type": "boolean",
@@ -1752,21 +1737,11 @@
"default": [],
"items": {}
},
"daemonsetAnnotations": {
"type": "object",
"description": "Annotations for orborus daemonset",
"default": {}
},
"deploymentAnnotations": {
"type": "object",
"description": "Annotations for orborus deployment",
"default": {}
},
"statefulsetAnnotations": {
"type": "object",
"description": "Annotations for orborus statefulset",
"default": {}
},
"podLabels": {
"type": "object",
"description": "Extra labels for orborus pods",
@@ -1829,16 +1804,11 @@
"properties": {
"type": {
"type": "string",
"description": "orborus statefulset strategy type",
"description": "orborus deployment strategy type",
"default": "RollingUpdate"
}
}
},
"podManagementPolicy": {
"type": "string",
"description": "Pod management policy for orborus statefulset",
"default": "OrderedReady"
},
"priorityClassName": {
"type": "string",
"description": "orborus pods' priorityClassName",
@@ -2092,6 +2062,11 @@
"description": "worker image repository",
"default": "shuffle/shuffle-worker"
},
"tag": {
"type": "string",
"description": "worker image tag (immutable tags are recommended, defaults to appVersion)",
"default": ""
},
"digest": {
"type": "string",
"description": "worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)",
@@ -2591,6 +2566,11 @@
"description": "OS Shell + Utility image repository",
"default": "bitnami/os-shell"
},
"tag": {
"type": "string",
"description": "OS Shell + Utility image tag (immutable tags are recommended)",
"default": "12-debian-12-r30"
},
"pullPolicy": {
"type": "string",
"description": "OS Shell + Utility image pull policy",
+18 -76
View File
@@ -96,7 +96,7 @@ backend:
## backend image
## @param backend.image.registry backend image registry
## @param backend.image.repository backend image repository
## @skip backend.image.tag backend image tag (immutable tags are recommended)
## @param backend.image.tag backend image tag (immutable tags are recommended, defaults to appVersion)
## @param backend.image.digest backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)
## @param backend.image.pullPolicy backend image pull policy
## @param backend.image.pullSecrets backend image pull secrets
@@ -104,7 +104,7 @@ backend:
image:
registry: ghcr.io
repository: shuffle/shuffle-backend
tag: nightly
tag: ""
digest: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
@@ -235,8 +235,8 @@ backend:
containerSecurityContext:
enabled: true
seLinuxOptions: {}
runAsUser: 1000
runAsGroup: 1000
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
privileged: false
@@ -260,18 +260,10 @@ backend:
## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/
##
hostAliases: []
## @param backend.daemonsetAnnotations Annotations for backend daemonset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
daemonsetAnnotations: {}
## @param backend.deploymentAnnotations Annotations for backend deployment
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
deploymentAnnotations: {}
## @param backend.statefulsetAnnotations Annotations for backend statefulset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
statefulsetAnnotations: {}
## @param backend.podLabels Extra labels for backend pods
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
##
@@ -321,22 +313,11 @@ backend:
## ONLY FOR DEPLOYMENTS:
## @param backend.updateStrategy.type backend deployment strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
## ONLY FOR STATEFULSETS:
## @param backend.updateStrategy.type backend statefulset strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
##
updateStrategy:
## ONLY FOR DEPLOYMENTS:
## Can be set to RollingUpdate or Recreate
## ONLY FOR STATEFULSETS:
## Can be set to RollingUpdate or OnDelete
##
type: RollingUpdate
## ONLY FOR STATEFULSETS:
## @param backend.podManagementPolicy Pod management policy for backend statefulset
## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies
##
podManagementPolicy: OrderedReady
## @param backend.priorityClassName backend pods' priorityClassName
##
priorityClassName: ""
@@ -565,7 +546,7 @@ frontend:
## frontend image
## @param frontend.image.registry frontend image registry
## @param frontend.image.repository frontend image repository
## @skip frontend.image.tag frontend image tag (immutable tags are recommended)
## @param frontend.image.tag frontend image tag (immutable tags are recommended, defaults to appVersion)
## @param frontend.image.digest frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)
## @param frontend.image.pullPolicy frontend image pull policy
## @param frontend.image.pullSecrets frontend image pull secrets
@@ -573,7 +554,7 @@ frontend:
image:
registry: ghcr.io
repository: shuffle/shuffle-frontend
tag: nightly
tag: ""
digest: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
@@ -683,11 +664,12 @@ frontend:
## @param frontend.podSecurityContext.fsGroup Set fsGroup in frontend pods' Security Context
##
podSecurityContext:
enabled: false
enabled: false # The default shuffle frontend image does not support running as non-root, because /etc/nginx/nginx.conf is written on startup
fsGroupChangePolicy: Always
sysctls: []
supplementalGroups: []
fsGroup: 1001
## Configure Container Security Context
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container
## @param frontend.containerSecurityContext.enabled Enabled frontend container' Security Context
@@ -702,10 +684,10 @@ frontend:
## @param frontend.containerSecurityContext.seccompProfile.type Set seccomp profile in frontend container
##
containerSecurityContext:
enabled: false
enabled: false # The default shuffle frontend image does not support running as non-root, because /etc/nginx/nginx.conf is written on startup
seLinuxOptions: {}
runAsUser: 101
runAsGroup: 101
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
privileged: false
@@ -728,18 +710,10 @@ frontend:
## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/
##
hostAliases: []
## @param frontend.daemonsetAnnotations Annotations for frontend daemonset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
daemonsetAnnotations: {}
## @param frontend.deploymentAnnotations Annotations for frontend deployment
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
deploymentAnnotations: {}
## @param frontend.statefulsetAnnotations Annotations for frontend statefulset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
statefulsetAnnotations: {}
## @param frontend.podLabels Extra labels for frontend pods
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
##
@@ -786,25 +760,13 @@ frontend:
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## ONLY FOR DEPLOYMENTS:
## @param frontend.updateStrategy.type frontend deployment strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
## ONLY FOR STATEFULSETS:
## @param frontend.updateStrategy.type frontend statefulset strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
##
updateStrategy:
## ONLY FOR DEPLOYMENTS:
## Can be set to RollingUpdate or Recreate
## ONLY FOR STATEFULSETS:
## Can be set to RollingUpdate or OnDelete
##
type: RollingUpdate
## ONLY FOR STATEFULSETS:
## @param frontend.podManagementPolicy Pod management policy for frontend statefulset
## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies
##
podManagementPolicy: OrderedReady
## @param frontend.priorityClassName frontend pods' priorityClassName
##
priorityClassName: ""
@@ -970,7 +932,7 @@ orborus:
## orborus image
## @param orborus.image.registry orborus image registry
## @param orborus.image.repository orborus image repository
## @skip orborus.image.tag orborus image tag (immutable tags are recommended)
## @param orborus.image.tag orborus image tag (immutable tags are recommended, defaults to appVersion)
## @param orborus.image.digest orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)
## @param orborus.image.pullPolicy orborus image pull policy
## @param orborus.image.pullSecrets orborus image pull secrets
@@ -978,7 +940,7 @@ orborus:
image:
registry: ghcr.io
repository: shuffle/shuffle-orborus
tag: nightly
tag: ""
digest: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
@@ -1107,8 +1069,8 @@ orborus:
containerSecurityContext:
enabled: true
seLinuxOptions: {}
runAsUser: 101
runAsGroup: 101
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
privileged: false
@@ -1132,18 +1094,10 @@ orborus:
## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/
##
hostAliases: []
## @param orborus.daemonsetAnnotations Annotations for orborus daemonset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
daemonsetAnnotations: {}
## @param orborus.deploymentAnnotations Annotations for orborus deployment
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
deploymentAnnotations: {}
## @param orborus.statefulsetAnnotations Annotations for orborus statefulset
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
##
statefulsetAnnotations: {}
## @param orborus.podLabels Extra labels for orborus pods
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
##
@@ -1190,25 +1144,13 @@ orborus:
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## ONLY FOR DEPLOYMENTS:
## @param orborus.updateStrategy.type orborus deployment strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
## ONLY FOR STATEFULSETS:
## @param orborus.updateStrategy.type orborus statefulset strategy type
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
##
updateStrategy:
## ONLY FOR DEPLOYMENTS:
## Can be set to RollingUpdate or Recreate
## ONLY FOR STATEFULSETS:
## Can be set to RollingUpdate or OnDelete
##
type: RollingUpdate
## ONLY FOR STATEFULSETS:
## @param orborus.podManagementPolicy Pod management policy for orborus statefulset
## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies
##
podManagementPolicy: OrderedReady
## @param orborus.priorityClassName orborus pods' priorityClassName
##
priorityClassName: ""
@@ -1377,13 +1319,13 @@ worker:
## worker image
## @param worker.image.registry worker image registry
## @param worker.image.repository worker image repository
## @skip worker.image.tag worker image tag (immutable tags are recommended)
## @param worker.image.tag worker image tag (immutable tags are recommended, defaults to appVersion)
## @param worker.image.digest worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)
##
image:
registry: ghcr.io
repository: shuffle/shuffle-worker
tag: nightly
tag: ""
digest: ""
## ServiceAccount configuration
@@ -1748,7 +1690,7 @@ volumePermissions:
## ref: https://hub.docker.com/r/bitnami/os-shell/tags/
## @param volumePermissions.image.registry OS Shell + Utility image registry
## @param volumePermissions.image.repository OS Shell + Utility image repository
## @skip volumePermissions.image.tag OS Shell + Utility image tag (immutable tags are recommended)
## @param volumePermissions.image.tag OS Shell + Utility image tag (immutable tags are recommended)
## @param volumePermissions.image.pullPolicy OS Shell + Utility image pull policy
## @param volumePermissions.image.pullSecrets OS Shell + Utility image pull secrets
##
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/docker/docker v27.5.0+incompatible
github.com/docker/go-connections v0.5.0
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.18
github.com/shuffle/shuffle-shared v0.8.35
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
)
+2 -2
View File
@@ -301,8 +301,8 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fc
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.18 h1:7f7cV+P2pr/g44i+AI8P0UheEe8oG8O2V8loBfe9YSw=
github.com/shuffle/shuffle-shared v0.8.18/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/shuffle/shuffle-shared v0.8.35 h1:3awc0TrsLLZiQeWD2XGIkTnFbczAG0cMfy1+cB/P7zg=
github.com/shuffle/shuffle-shared v0.8.35/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+183 -164
View File
@@ -1,13 +1,8 @@
package main
/*
Orborus exists to listen for new jobs which are deployed as workers.
Orborus exists to listen for new jobs from Shuffle. This is to run workflows, pipelines, and other tasks.
*/
// Potential issues:
// Default network could be same as on the host
// Ingress network may not exist (default)
import (
"archive/zip"
"bytes"
@@ -60,7 +55,7 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
// Starts jobs in bulk, so this could be increased
// Starts jobs in bulk, so this could be increased or decreased based on who the user is
var sleepTime = 2
// Making it work on low-end machines even during busy times :)
@@ -75,9 +70,13 @@ var workerVersion = os.Getenv("SHUFFLE_WORKER_VERSION")
var newWorkerImage = os.Getenv("SHUFFLE_WORKER_IMAGE")
var dockerSwarmBridgeMTU = os.Getenv("SHUFFLE_SWARM_BRIDGE_DEFAULT_MTU")
var dockerSwarmBridgeInterface = os.Getenv("SHUFFLE_SWARM_BRIDGE_DEFAULT_INTERFACE")
var maxCPUPercent = 90
// Kubernetes settings
var isKubernetes = os.Getenv("IS_KUBERNETES")
var kubernetesNamespace = os.Getenv("KUBERNETES_NAMESPACE")
var maxCPUPercent = 90
var workerServiceAccountName = os.Getenv("SHUFFLE_WORKER_SERVICE_ACCOUNT_NAME")
var appServiceAccountName = os.Getenv("SHUFFLE_APP_SERVICE_ACCOUNT_NAME")
// var baseimagename = "docker.pkg.github.com/shuffle/shuffle"
// var baseimagename = "ghcr.io/frikky"
@@ -217,8 +216,7 @@ func skipCheckInCleanup(name string) bool {
func cleanupExistingNodes(ctx context.Context) error {
if isKubernetes == "true" {
// of course, this doesn't clean up "nodes" but
// rather pods, services, roles etc.
// Cleanup all workers created by orborus and all apps created by workers.
if kubernetesNamespace == "" {
kubernetesNamespace = "default"
@@ -230,62 +228,38 @@ func cleanupExistingNodes(ctx context.Context) error {
return err
}
// Delete all pods
pods, err := clientset.CoreV1().Pods(kubernetesNamespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
log.Printf("[ERROR] Failed listing pods: %s", err)
return err
}
for _, pod := range pods.Items {
// check if pod.Name starts with:
// "backend-", "frontend-", "orborus-", "opensearch-" or "memcached-"
if skipCheckInCleanup(pod.Name) {
continue
}
err := clientset.CoreV1().Pods(kubernetesNamespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
if err != nil {
log.Printf("[ERROR] Failed deleting pod %s: %s", pod.Name, err)
}
}
// Delete all services
services, err := clientset.CoreV1().Services(kubernetesNamespace).List(context.Background(), metav1.ListOptions{})
services, err := clientset.CoreV1().Services(kubernetesNamespace).List(context.Background(), metav1.ListOptions{
LabelSelector: "app.kubernetes.io/name in (shuffle-worker, shuffle-app),app.kubernetes.io/managed-by in (shuffle-orborus, shuffle-worker)",
})
if err != nil {
log.Printf("[ERROR] Failed listing services: %s", err)
return err
}
for _, service := range services.Items {
if skipCheckInCleanup(service.Name) {
continue
}
err := clientset.CoreV1().Services(kubernetesNamespace).Delete(context.Background(), service.Name, metav1.DeleteOptions{})
if err != nil {
log.Printf("[ERROR] Failed deleting service %s: %s", service.Name, err)
}
}
deployments, err := clientset.AppsV1().Deployments(kubernetesNamespace).List(context.Background(), metav1.ListOptions{})
deployments, err := clientset.AppsV1().Deployments(kubernetesNamespace).List(context.Background(), metav1.ListOptions{
LabelSelector: "app.kubernetes.io/name in (shuffle-worker, shuffle-app),app.kubernetes.io/managed-by in (shuffle-orborus, shuffle-worker)",
})
if err != nil {
log.Printf("[ERROR] Failed listing deployments: %s", err)
return err
}
for _, deployment := range deployments.Items {
if skipCheckInCleanup(deployment.Name) {
continue
}
err := clientset.AppsV1().Deployments(kubernetesNamespace).Delete(context.Background(), deployment.Name, metav1.DeleteOptions{})
if err != nil {
log.Printf("[ERROR] Failed deleting deployment %s: %s", deployment.Name, err)
}
}
log.Printf("[INFO] Cleaned up all pods and services in namespace %s. Waiting 10 seconds for cleanup to reflect", kubernetesNamespace)
log.Printf("[INFO] Cleaned up all services and deployments in namespace %s. Waiting 10 seconds for cleanup to reflect", kubernetesNamespace)
time.Sleep(10 * time.Second)
@@ -457,19 +431,19 @@ func deployServiceWorkers(image string) {
}
/*
isMemcachedRunning, err := checkMemcached(ctx, dockercli)
if err != nil {
log.Printf("[ERROR] Failed checking memcached: %s", err)
}
if isMemcachedRunning == false {
log.Printf("[ERROR] Memcached is not running. Will try to deploy it.")
deployMemcached(dockercli)
}
isMemcachedRunning, err := checkMemcached(ctx, dockercli)
if err != nil {
log.Printf("[ERROR] Failed checking memcached: %s", err)
}
if isMemcachedRunning == false {
log.Printf("[ERROR] Memcached is not running. Will try to deploy it.")
deployMemcached(dockercli)
}
ip := "shuffle-cache"
if len(os.Getenv("SHUFFLE_MEMCACHED")) == 0 {
os.Setenv("SHUFFLE_MEMCACHED", fmt.Sprintf("%s:11211", ip))
}
ip := "shuffle-cache"
if len(os.Getenv("SHUFFLE_MEMCACHED")) == 0 {
os.Setenv("SHUFFLE_MEMCACHED", fmt.Sprintf("%s:11211", ip))
}
*/
defaultNetworkAttach := false
@@ -725,7 +699,6 @@ func deployServiceWorkers(image string) {
if err == nil {
log.Printf("[DEBUG] Successfully deployed workers with %d replica(s) on %d node(s)", replicas, cnt)
//time.Sleep(time.Duration(10) * time.Second)
//log.Printf("[DEBUG] Servicecreate request: %#v %#v", service, err)
} else {
if !strings.Contains(fmt.Sprintf("%s", err), "Already Exists") && !strings.Contains(fmt.Sprintf("%s", err), "is already in use by service") {
@@ -769,10 +742,10 @@ func handleBackendImageDownload(ctx context.Context, images string) error {
// Remove the image
handled := []string{}
log.Printf("[DEBUG] Removing existing image (s): %s. Waiting 30 seconds before starting to ensure backend has the latest images built and ready to distribute.", images)
//time.Sleep(time.Duration(30) * time.Second)
//log.Printf("[DEBUG] Removing existing image (s): %s", images)
newImages := []string{}
successful := []string{}
for _, curimage := range strings.Split(images, ",") {
curimage = strings.TrimSpace(curimage)
if shuffle.ArrayContains(handled, curimage) {
@@ -787,25 +760,36 @@ func handleBackendImageDownload(ctx context.Context, images string) error {
newImages = append(newImages, curimage)
// Force remove the current image to avoid cached layers
_, err := dockercli.ImageRemove(ctx, curimage, image.RemoveOptions{
Force: true,
PruneChildren: true,
})
if swarmConfig == "run" || swarmConfig == "swarm" {
_, err := dockercli.ImageRemove(ctx, curimage, image.RemoveOptions{
Force: true,
PruneChildren: true,
})
if err != nil {
log.Printf("[ERROR] Failed removing image for re-download: %s", err)
if err != nil {
log.Printf("[ERROR] Failed removing image for re-download: %s", err)
} else {
log.Printf("[DEBUG] Removed image: %s", curimage)
}
} else {
log.Printf("[DEBUG] Removed image: %s", curimage)
//log.Printf("[DEBUG] Skipping image removal for %s as swarmConfig is not set to run or swarm. Value: %#v", curimage, swarmConfig)
}
err = shuffle.DownloadDockerImageBackend(&http.Client{Timeout: imagedownloadTimeout}, curimage)
err := shuffle.DownloadDockerImageBackend(&http.Client{Timeout: imagedownloadTimeout}, curimage)
if err != nil {
log.Printf("[ERROR] Failed downloading image: %s", err)
//log.Printf("[ERROR] Failed downloading image: %s", err)
} else {
log.Printf("[DEBUG] Downloaded image: %s", curimage)
//log.Printf("[DEBUG] Downloaded image: %s", curimage)
successful = append(successful, curimage)
}
}
if len(successful) == 0 {
log.Printf("[ERROR] Failed downloading image copies: %s. This means the app may not have been updated.", strings.Join(handled, ", "))
} else {
log.Printf("[DEBUG] Successfully downloaded image copies: %s", strings.Join(successful, ", "))
}
if swarmConfig == "run" || swarmConfig == "swarm" {
log.Printf("[DEBUG] Should update service with new image after updating(s): %s. \n\nBETA REPLACEMENT IMPLEMENTATION: Contact support@shuffler.io for support.", strings.Join(newImages, "\n"))
@@ -853,7 +837,7 @@ func handleBackendImageDownload(ctx context.Context, images string) error {
if !strings.Contains(fmt.Sprintf("%s", resp), "error") {
break
} else {
found = true
found = true
log.Printf("[ERROR] Failed updating service %s with the new image %s: %s. Resp: %#v", service.Spec.Annotations.Name, image, err, resp)
}
}
@@ -1010,6 +994,10 @@ func deployK8sWorker(image string, identifier string, env []string) error {
env = append(env, fmt.Sprintf("SHUFFLE_USE_GHCR_OVERRIDE_FOR_AUTODEPLOY=%s", os.Getenv("SHUFFLE_USE_GHCR_OVERRIDE_FOR_AUTODEPLOY")))
}
if len(appServiceAccountName) > 0 {
env = append(env, fmt.Sprintf("SHUFFLE_APP_SERVICE_ACCOUNT_NAME=%s", appServiceAccountName))
}
clientset, _, err := shuffle.GetKubernetesClient()
if err != nil {
log.Printf("[ERROR] Error getting kubernetes client:", err)
@@ -1018,18 +1006,6 @@ func deployK8sWorker(image string, identifier string, env []string) error {
//env = append(env, fmt.Sprintf("KUBERNETES_CONFIG=%s", config.String()))
// FIXME: When a service account is used, the account is also mounted in the pod
// The volume mount location is:
// /var/run/secrets/kubernetes.io/serviceaccount
// Look for if there is a default service account in use
if len(os.Getenv("KUBERNETES_SERVICE_ACCOUNT")) > 0 {
log.Printf("[DEBUG] Using Kubernetes service account %s", os.Getenv("KUBERNETES_SERVICE_ACCOUNT"))
env = append(env, fmt.Sprintf("KUBERNETES_SERVICE_ACCOUNT=%s", os.Getenv("KUBERNETES_SERVICE_ACCOUNT")))
// use k8s downward API to find it if we are in a pod
}
// Check if namespace exist as variable. If so, make it
if len(os.Getenv("KUBERNETES_NAMESPACE")) > 0 && !namespacemade {
kubernetesNamespace = os.Getenv("KUBERNETES_NAMESPACE")
@@ -1088,10 +1064,21 @@ func deployK8sWorker(image string, identifier string, env []string) error {
}
}
containerLabels := map[string]string{
labels := map[string]string{
"app.kubernetes.io/name": "shuffle-worker",
"app.kubernetes.io/instance": identifier,
// "app.kubernetes.io/version": "",
"app.kubernetes.io/part-of": "shuffle",
"app.kubernetes.io/managed-by": "shuffle-orborus",
// Keep legacy labels for backward compatibility
"container": "shuffle-worker",
}
matchLabels := map[string]string{
"app.kubernetes.io/name": "shuffle-worker",
"app.kubernetes.io/instance": identifier,
}
containerAttachment := corev1.Container{
Name: identifier,
Image: kubernetesImage,
@@ -1194,22 +1181,24 @@ func deployK8sWorker(image string, identifier string, env []string) error {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: identifier,
Name: identifier,
Labels: labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicaNumberInt32,
Selector: &metav1.LabelSelector{
MatchLabels: containerLabels,
MatchLabels: matchLabels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: containerLabels,
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerAttachment,
},
DNSPolicy: corev1.DNSClusterFirst,
DNSPolicy: corev1.DNSClusterFirst,
ServiceAccountName: workerServiceAccountName,
},
},
},
@@ -1224,10 +1213,11 @@ func deployK8sWorker(image string, identifier string, env []string) error {
// kubectl expose deployment shuffle-workers --type=NodePort --port=33333 --target-port=33333
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: identifier,
Name: identifier,
Labels: labels,
},
Spec: corev1.ServiceSpec{
Selector: containerLabels,
Selector: matchLabels,
Ports: []corev1.ServicePort{
{
Protocol: "TCP",
@@ -1250,7 +1240,6 @@ func deployK8sWorker(image string, identifier string, env []string) error {
func deployWorker(image string, identifier string, env []string, executionRequest shuffle.ExecutionRequest) error {
if len(os.Getenv("REGISTRY_URL")) > 0 && os.Getenv("REGISTRY_URL") != "" {
env = append(env, fmt.Sprintf("REGISTRY_URL=%s", os.Getenv("REGISTRY_URL")))
}
@@ -1280,18 +1269,17 @@ func deployWorker(image string, identifier string, env []string, executionReques
Resources: container.Resources{},
}
certPath := "/certs"
// This is just to test the mounting locally so
// I can control from what source I'm mounting
// the certs to. Default behaviour is:
// /certs:/certs.
certPath := "/certs"
if os.Getenv("SHUFFLE_CERT_PATH") != "" {
certPath = os.Getenv("SHUFFLE_CERT_PATH")
}
_, err := os.ReadDir(certPath)
if certPath != "" && err == nil {
certVol := mount.Mount{
Type: mount.TypeBind,
@@ -1310,7 +1298,6 @@ func deployWorker(image string, identifier string, env []string, executionReques
}
}
//var swarmConfig = os.Getenv("SHUFFLE_SWARM_CONFIG")
parsedUuid := uuid.NewV4()
@@ -1322,7 +1309,7 @@ func deployWorker(image string, identifier string, env []string, executionReques
if isKubernetes != "true" {
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", containerId))
if strings.ToLower(cleanupEnv) != "false" {
if strings.ToLower(cleanupEnv) == "true" {
hostConfig.AutoRemove = true
}
}
@@ -1351,11 +1338,11 @@ func deployWorker(image string, identifier string, env []string, executionReques
)
if err != nil {
log.Printf("[ERROR] Container create error(2): %s", err)
log.Printf("[ERROR][%s] Container create error(2): %s", executionRequest.ExecutionId, err)
return err
}
} else {
log.Printf("[ERROR] Container create error: %s", err)
log.Printf("[ERROR][%s] Container create error: %s", executionRequest.ExecutionId, err)
return err
}
}
@@ -1380,47 +1367,47 @@ func deployWorker(image string, identifier string, env []string, executionReques
identifier+"-2",
)
if err != nil {
log.Printf("[ERROR] Failed to CREATE container (2): %s", err)
log.Printf("[ERROR][%s] Failed to CREATE container (2): %s", executionRequest.ExecutionId, err)
}
err = dockercli.ContainerStart(context.Background(), cont.ID, containerStartOptions)
if err != nil {
log.Printf("[ERROR] Failed to start container (2): %s", err)
log.Printf("[ERROR][%s] Failed to start container (2): %s", executionRequest.ExecutionId, err)
}
} else {
log.Printf("[ERROR] Failed initial container start. Quitting as this is NOT a simple network issue. Err: %s", err)
log.Printf("[ERROR][%s] Failed initial container start. Quitting as this is NOT a simple network issue. Err: %s", executionRequest.ExecutionId, err)
}
if err != nil {
log.Printf("[ERROR] Failed to start worker container in environment '%s': %s", environment, err)
log.Printf("[ERROR][%s] Failed to start worker container in environment '%s': %s", executionRequest.ExecutionId, environment, err)
return err
} else {
log.Printf("[INFO][%s] Worker Container created (2). Environment %s: docker logs %s", executionRequest.ExecutionId, environment, cont.ID)
log.Printf("[INFO][%s] Worker Container created (2). Runtime Location '%s': docker logs -f %s", executionRequest.ExecutionId, environment, cont.ID)
}
stats, err := dockercli.ContainerInspect(ctx, cont.ID)
if err != nil {
log.Printf("[WARNING] Failed checking worker '%s': %s", cont.ID, err)
return nil
log.Printf("[WARNING][%s] Failed checking worker '%s': %s", executionRequest.ExecutionId, cont.ID, err)
return nil
}
containerStatus := stats.ContainerJSONBase.State.Status
if containerStatus != "running" {
log.Printf("[ERROR] Status of %s is %s. Should be running. Contact support@shuffler.io if this persists.", cont.ID, containerStatus)
log.Printf("[ERROR][%s] Status of %s is %s. Should be running. Contact support@shuffler.io if this persists.", executionRequest.ExecutionId, cont.ID, containerStatus)
}
/*
err = stopWorker(containerName)
if err != nil {
log.Printf("Failed stopping worker %s", execution.ExecutionId)
return nil
}
/*
err = stopWorker(containerName)
if err != nil {
log.Printf("Failed stopping worker %s", execution.ExecutionId)
return nil
}
err = deployWorker(dockercli, workerImage, containerName, env)
if err != nil {
log.Printf("Failed executing worker %s in state %s", execution.ExecutionId, containerStatus)
return nil
err = deployWorker(dockercli, workerImage, containerName, env)
if err != nil {
log.Printf("Failed executing worker %s in state %s", execution.ExecutionId, containerStatus)
return nil
}
}
}
*/
} else {
log.Printf("[INFO][%s] New Worker created. Environment %s: docker logs %s", executionRequest.ExecutionId, environment, cont.ID)
@@ -1468,31 +1455,43 @@ func initializeImages() {
}
if baseimageregistry == "" {
//baseimageregistry = "ghcr.io" // Github
baseimageregistry = "docker.io" // Dockerhub
baseimageregistry = "ghcr.io" // Github
log.Printf("[DEBUG] Setting baseimageregistry to %#v", baseimageregistry)
if len(os.Getenv("REGISTRY_URL")) > 0 {
baseimageregistry = os.Getenv("REGISTRY_URL")
} else {
os.Setenv("REGISTRY_URL", baseimageregistry)
}
os.Setenv("SHUFFLE_BASE_IMAGE_REGISTRY", baseimageregistry)
log.Printf("[WARNING] Setting baseimageregistry to %#v", baseimageregistry)
}
if baseimagename == "" {
// FIXME: This is probably the problem for image names tbh
//baseimagename = "shuffle" // Github (ghcr.io)
baseimagename = "frikky/shuffle" // Dockerhub
baseimagename = "shuffle" // Github (ghcr.io)
log.Printf("[DEBUG] Setting baseimagename to %#v", baseimagename)
os.Setenv("SHUFFLE_BASE_IMAGE_NAME", baseimagename)
log.Printf("[WARNING] Setting baseimagename to %#v", baseimagename)
}
log.Printf("[DEBUG] Setting swarm config to %#v. Default is empty.", swarmConfig)
newWorker := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
// This is now always static
newWorker := fmt.Sprintf("ghcr.io/shuffle/shuffle-worker:%s", workerVersion)
if len(newWorkerImage) > 0 {
newWorker = newWorkerImage
}
// check whether they are the same first
if os.Getenv("SHUFFLE_AUTO_IMAGE_DOWNLOAD") != "true" {
// Check whether they are the same first
if os.Getenv("SHUFFLE_AUTO_IMAGE_DOWNLOAD") == "false" {
log.Printf("[DEBUG] Skipping image download as SHUFFLE_AUTO_IMAGE_DOWNLOAD is set to false")
} else {
images := []string{
fmt.Sprintf("frikky/shuffle:app_sdk"),
fmt.Sprintf("shuffle/shuffle:app_sdk"),
fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", baseimageregistry, baseimagename, appSdkVersion),
newWorker,
}
@@ -1513,8 +1512,6 @@ func initializeImages() {
log.Printf("[DEBUG] Successfully downloaded and built %s", image)
}
}
} else {
log.Printf("[DEBUG] Skipping image download as SHUFFLE_AUTO_IMAGE_DOWNLOAD is set to true")
}
}
@@ -1931,7 +1928,7 @@ func main() {
}
// Handle Cleanup - made it cleanup by default
if strings.ToLower(os.Getenv("SHUFFLE_CONTAINER_AUTO_CLEANUP")) != "false" {
if strings.ToLower(os.Getenv("SHUFFLE_CONTAINER_AUTO_CLEANUP")) != "false" && os.Getenv("CLEANUP") == "" {
cleanupEnv = "true"
}
@@ -2008,7 +2005,7 @@ func main() {
log.Printf("[INFO] Setting up Docker environment. Downloading worker and App SDK!")
initializeImages()
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
workerImage := fmt.Sprintf("ghcr.io/shuffle/shuffle-worker:%s", workerVersion)
if len(newWorkerImage) > 0 {
workerImage = newWorkerImage
}
@@ -2119,6 +2116,7 @@ func main() {
// Marshal and set body
orborusStats := getOrborusStats(ctx)
pipelinePayload, pipelineerr := sendPipelineHealthStatus()
if pipelineerr != nil {
@@ -2248,7 +2246,7 @@ func main() {
if incRequest.Type == "PIPELINE_CREATE" || incRequest.Type == "PIPELINE_START" || incRequest.Type == "PIPELINE_STOP" || incRequest.Type == "PIPELINE_DELETE" {
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
tenzirDisabled = false
tenzirDisabled = false
// Running NEW or editing pipelines
err := handlePipeline(incRequest)
@@ -2258,12 +2256,9 @@ func main() {
toBeRemoved.Data = append(toBeRemoved.Data, incRequest)
} else if incRequest.Type == "DOCKER_IMAGE_DOWNLOAD" {
log.Printf("[INFO] Re-downloading new image(s): %#v", incRequest.ExecutionArgument)
log.Printf("[INFO] Re-downloading new image(s) due to backend request: %#v", incRequest.ExecutionArgument)
if len(incRequest.ExecutionArgument) > 0 {
// FIXME: Wait X seconds before running this as the image build may not be done yet. This is shitty, but may be ok to do in Orborus. Easy fix for the future: Just let it run through jobs 5-10 times before actually picking it up
// Run after 25 seconds in the goroutine instead
go handleBackendImageDownload(ctx, incRequest.ExecutionArgument)
} else {
log.Printf("[ERROR] No image name provided for download. Removing job from queue.")
@@ -2273,7 +2268,7 @@ func main() {
} else if incRequest.Type == "CATEGORY_UPDATE" {
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
tenzirDisabled = false
tenzirDisabled = false
err = handleFileCategoryChange()
if err != nil {
@@ -2318,7 +2313,7 @@ func main() {
// Manual command = overrides to allow starting of Tenzir from the frontend anyway.
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
tenzirDisabled = false
tenzirDisabled = false
// Removed either way
toBeRemoved.Data = append(toBeRemoved.Data, incRequest)
@@ -2328,7 +2323,7 @@ func main() {
if strings.Contains(fmt.Sprintf("%s", err), "node available") {
// Disabling until UI is updated
os.Setenv("SHUFFLE_SKIP_PIPELINES", "true")
tenzirDisabled = true
tenzirDisabled = true
log.Printf("[ERROR] Failed to start tenzir, reason: %s", err)
err = shuffle.CreateOrgNotification(
@@ -2426,11 +2421,11 @@ func main() {
}
if execution.Status == "ABORT" || execution.Status == "FAILED" {
log.Printf("[INFO] Executionstatus issue: ", execution.Status)
log.Printf("[INFO][%s] Executionstatus issue: ", execution.ExecutionId, execution.Status)
}
if shuffle.ArrayContains(executionIds, execution.ExecutionId) {
log.Printf("[INFO] Execution already handled (rerun of old executions?): %s", execution.ExecutionId)
log.Printf("[INFO][%s] Execution already handled (rerunning old execution)", execution.ExecutionId)
toBeRemoved.Data = append(toBeRemoved.Data, execution)
// Should check when last this was ran, and if it's more than 10 minutes ago and it's not finished, we should run it again?
@@ -2490,7 +2485,7 @@ func main() {
// Look for volume binds
if len(os.Getenv("SHUFFLE_VOLUME_BINDS")) > 0 {
log.Printf("[DEBUG] Added volume binds: %s", os.Getenv("SHUFFLE_VOLUME_BINDS"))
//log.Printf("[DEBUG] Added volume binds: %s", os.Getenv("SHUFFLE_VOLUME_BINDS"))
env = append(env, fmt.Sprintf("SHUFFLE_VOLUME_BINDS=%s", os.Getenv("SHUFFLE_VOLUME_BINDS")))
}
@@ -2520,10 +2515,24 @@ func main() {
toBeRemoved.Data = append(toBeRemoved.Data, execution)
executionIds = append(executionIds, execution.ExecutionId)
} else {
log.Printf("[WARNING] Execution ID '%s' failed to deploy: %s", execution.ExecutionId, err)
log.Printf("[WARNING][%s] Failed to deploy: %s", execution.ExecutionId, err)
if strings.Contains(err.Error(), "already exists") {
toBeRemoved.Data = append(toBeRemoved.Data, execution)
executionIds = append(executionIds, execution.ExecutionId)
} else if strings.Contains(err.Error(), "No such image") {
// Download the image
if isKubernetes == "true" {
log.Printf("[DEBUG] Skipping image pull of '%s' because Kubernetes does it in realtime instead", workerImage)
} else {
log.Printf("[DEBUG] Re-pulling image %s as it doesn't exist, and is necessary for worker to run (autofix)", workerImage)
pullOptions := image.PullOptions{}
_, err = dockercli.ImagePull(ctx, workerImage, pullOptions)
if err != nil {
log.Printf("[ERROR] Failed to pull image %s: %s", workerImage, err)
}
}
}
}
}
@@ -2647,7 +2656,7 @@ func deployTenzirNode() error {
// return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
//log.Printf("[INFO] Pipelines are enabled by user")
} else {
return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
}
if isKubernetes == "true" {
@@ -2847,12 +2856,12 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
},
}
// FIXME: Is this necessary? Seems to screw up networking:
// FIXME: Is this necessary? Seems to screw up networking:
// conflicting options: hostname and the network mode
/*
if isKubernetes != "true" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" {
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", containerId))
}
if isKubernetes != "true" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" {
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", containerId))
}
*/
resp, err := dockercli.ContainerCreate(ctx, config, hostConfig, networkingConfig, nil, containerName)
@@ -3060,7 +3069,6 @@ func createPipeline(command, identifier string) (string, error) {
return "", err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("[ERROR] Failed reading response body: %s", err)
@@ -3078,8 +3086,8 @@ func createPipeline(command, identifier string) (string, error) {
}
type PipelineResponse struct {
ID string `json:"id"`
Message string `json:"message"`
ID string `json:"id"`
Message string `json:"message"`
Severity string `json:"severity"`
}
@@ -3215,10 +3223,28 @@ func deletePipeline(pipelineId string) error {
func listPipelines() ([]shuffle.PipelineInfo, error) {
responseData := shuffle.PipelineInfoWrapper{}
if tenzirDisabled {
return responseData.Pipelines, errors.New("Tenzir is disabled")
}
var reqBody []byte
url := fmt.Sprintf("%s/api/v0/pipeline/list", pipelineUrl)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
client := http.Client{
Timeout: 2 * time.Second,
}
req, err := http.NewRequest(
"POST",
url,
bytes.NewBuffer(reqBody),
)
if err != nil {
return responseData.Pipelines, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return responseData.Pipelines, err
}
@@ -3302,7 +3328,7 @@ func handleFileCategoryChange() error {
tenzirStorageFolder = "/tmp/"
}
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder)
err = extractZIP("files.zip", sigmaPath)
if err != nil {
@@ -3310,7 +3336,6 @@ func handleFileCategoryChange() error {
return err
}
log.Printf("[DEBUG] Detection files copied to '%s' successfully.", sigmaPath)
return nil
@@ -3399,7 +3424,7 @@ func removeFileCategory() error {
tenzirStorageFolder = "/tmp/"
}
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
//sigmaPath := "/var/lib/tenzir/sigma_rules/*"
sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder)
@@ -3446,6 +3471,10 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
Pipelines: []shuffle.PipelineInfoMini{},
}
if tenzirDisabled {
return pipelinePayload, nil
}
// To not spam down the list API too much
randint := rand.Intn(5)
if len(pipelines) == 0 || randint == 0 {
@@ -3468,10 +3497,6 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
pipelinePayload.Pipelines = pipelines
}
if tenzirDisabled {
return pipelinePayload, nil
}
err := deployTenzirNode()
if err != nil {
if (!strings.Contains(err.Error(), "SHUFFLE_SKIP_PIPELINES") && !strings.Contains(err.Error(), "Kubernetes not implemented for Tenzir node")) && !strings.Contains(err.Error(), "Tenzir Node is already running") && !strings.Contains(err.Error(), "docker daemon") {
@@ -3564,9 +3589,8 @@ func getRunningWorkers(ctx context.Context, workerTimeout int) int {
return 0
}
labelSelector := "app=shuffle-worker"
pods, podErr := clientset.CoreV1().Pods(kubernetesNamespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
LabelSelector: "app.kubernetes.io/name=shuffle-worker",
})
if podErr != nil {
log.Printf("[ERROR] Failed getting running workers: %s", podErr)
@@ -3655,15 +3679,12 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
All: true,
})
//log.Printf("Len: %d", len(containers))
if err != nil {
log.Printf("[ERROR] Failed creating Containerlist: %s", err)
return err
}
containerNames := map[string]string{}
stopContainers := []string{}
removeContainers := []string{}
log.Printf("[INFO] Baseimage: %s, Workertimeout: %d", baseimagename, int64(workerTimeout))
@@ -3812,8 +3833,7 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
if err != nil {
log.Printf("[ERROR] Failed creating worker request: %s", err)
if strings.Contains(fmt.Sprintf("%s", err), "connection refused") || strings.Contains(fmt.Sprintf("%s", err), "EOF") {
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
workerImage := fmt.Sprintf("ghcr.io/shuffle/shuffle-worker:%s", workerVersion)
if len(newWorkerImage) > 0 {
workerImage = newWorkerImage
}
@@ -3837,8 +3857,7 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
log.Printf("[ERROR] Error running worker request to %s (1): %s", streamUrl, err)
if strings.Contains(fmt.Sprintf("%s", err), "connection refused") || strings.Contains(fmt.Sprintf("%s", err), "EOF") {
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
workerImage := fmt.Sprintf("ghcr.io/shuffle/shuffle-worker:%s", workerVersion)
if len(newWorkerImage) > 0 {
workerImage = newWorkerImage
}
+1 -1
View File
@@ -8,7 +8,7 @@ require (
github.com/docker/docker v27.5.0+incompatible
github.com/gorilla/mux v1.8.1
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.18
github.com/shuffle/shuffle-shared v0.8.35
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.2
+2 -2
View File
@@ -294,8 +294,8 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fc
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.18 h1:7f7cV+P2pr/g44i+AI8P0UheEe8oG8O2V8loBfe9YSw=
github.com/shuffle/shuffle-shared v0.8.18/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/shuffle/shuffle-shared v0.8.35 h1:3awc0TrsLLZiQeWD2XGIkTnFbczAG0cMfy1+cB/P7zg=
github.com/shuffle/shuffle-shared v0.8.35/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+88 -44
View File
@@ -23,8 +23,8 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
dockerclient "github.com/docker/docker/client"
// This is for automatic removal of certain code :)
@@ -56,6 +56,7 @@ var logsDisabled = os.Getenv("SHUFFLE_LOGS_DISABLED")
var cleanupEnv = strings.ToLower(os.Getenv("CLEANUP"))
var swarmNetworkName = os.Getenv("SHUFFLE_SWARM_NETWORK_NAME")
var dockerApiVersion = strings.ToLower(os.Getenv("DOCKER_API_VERSION"))
var appServiceAccountName = os.Getenv("SHUFFLE_APP_SERVICE_ACCOUNT_NAME")
var kubernetesNamespace = os.Getenv("KUBERNETES_NAMESPACE")
var executionCount int64
@@ -104,9 +105,9 @@ var window = shuffle.NewTimeWindow(10 * time.Second)
// Images to be autodeployed in the latest version of Shuffle.
var autoDeploy = map[string]string{
"http:1.4.0": "frikky/shuffle:http_1.4.0",
"shuffle-tools:1.2.0": "frikky/shuffle:shuffle-tools_1.2.0",
"shuffle-subflow:1.1.0": "frikky/shuffle:shuffle-subflow_1.1.0",
"http:1.4.0": "frikky/shuffle:http_1.4.0",
"shuffle-tools:1.2.0": "frikky/shuffle:shuffle-tools_1.2.0",
"shuffle-subflow:1.1.0": "frikky/shuffle:shuffle-subflow_1.1.0",
// "shuffle-tools-fork:1.0.0": "frikky/shuffle:shuffle-tools-fork_1.0.0",
}
@@ -298,7 +299,7 @@ func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason
}
// Might not be necessary because of cleanupEnv hostconfig autoremoval
if cleanupEnv == "true" && (os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm") {
if strings.ToLower(cleanupEnv) == "true" && (os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm") {
/*
ctx := context.Background()
dockercli, err := dockerclient.NewEnvClient()
@@ -482,9 +483,24 @@ func deployk8sApp(image string, identifier string, env []string) error {
//fix naming convention
// podUuid := uuid.NewV4().String()
// podName := fmt.Sprintf("%s-%s", value, podUuid)
// name := fmt.Sprintf("%s-%s", value, podUuid)
// replace identifier "_" with "-"
podName := strings.ReplaceAll(identifier, "_", "-")
name := strings.ReplaceAll(identifier, "_", "-")
labels := map[string]string{
"app.kubernetes.io/name": "shuffle-app",
"app.kubernetes.io/instance": name,
// "app.kubernetes.io/version": "",
"app.kubernetes.io/part-of": "shuffle",
"app.kubernetes.io/managed-by": "shuffle-worker",
// Keep legacy labels for backward compatibility
"app": name,
}
matchLabels := map[string]string{
"app.kubernetes.io/name": "shuffle-app",
"app.kubernetes.io/instance": name,
}
// pod := &corev1.Pod{
// ObjectMeta: metav1.ObjectMeta{
@@ -564,20 +580,17 @@ func deployk8sApp(image string, identifier string, env []string) error {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Name: name,
Labels: labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicaNumberInt32,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": podName,
},
MatchLabels: matchLabels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": podName,
},
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@@ -587,6 +600,8 @@ func deployk8sApp(image string, identifier string, env []string) error {
Env: buildEnvVars(envMap),
},
},
DNSPolicy: corev1.DNSClusterFirst,
ServiceAccountName: appServiceAccountName,
},
},
},
@@ -601,12 +616,11 @@ func deployk8sApp(image string, identifier string, env []string) error {
// kubectl expose deployment {podName} --type=NodePort --port=80 --target-port=80
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Name: name,
Labels: labels,
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": podName,
},
Selector: matchLabels,
Ports: []corev1.ServicePort{
{
Protocol: "TCP",
@@ -870,7 +884,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
}
// Removing because log extraction should happen first
if cleanupEnv == "true" {
if strings.ToLower(cleanupEnv) == "true" {
hostConfig.AutoRemove = true
}
@@ -880,19 +894,30 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
if len(volumeBindString) > 0 {
volumeBindSplit := strings.Split(volumeBindString, ",")
for _, volumeBind := range volumeBindSplit {
if strings.Contains(volumeBind, ":") {
volumeBinds = append(volumeBinds, volumeBind)
} else {
log.Printf("[ERROR] Volume bind '%s' is invalid.", volumeBind)
if volumeBind == "srcfolder=dstfolder" || volumeBind == "srcfolder:dstfolder" || volumeBind == "/srcfolder:/dstfolder" {
log.Printf("[DEBUG] Volume bind '%s' is invalid.", volumeBind)
continue
}
if !strings.HasPrefix(volumeBind, "/") {
log.Printf("[ERROR] Volume bind '%s' is invalid. Use absolute paths.", volumeBind)
continue
}
if !strings.Contains(volumeBind, ":") {
log.Printf("[ERROR] Volume bind '%s' is invalid. Use absolute paths with colon inbetween them (/srcpath:dstpath/", volumeBind)
continue
}
volumeBinds = append(volumeBinds, volumeBind)
}
}
// Add more volume binds if possible
if len(volumeBinds) > 0 {
log.Printf("[DEBUG] Setting up binds for container. Got %d volume binds.", len(volumeBinds))
hostConfig.Binds = volumeBinds
// Only use mounts, not direct binds
hostConfig.Binds = []string{}
hostConfig.Mounts = []mount.Mount{}
for _, bind := range volumeBinds {
if !strings.Contains(bind, ":") || strings.Contains(bind, "..") || strings.HasPrefix(bind, "~") {
@@ -900,15 +925,27 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
continue
}
log.Printf("[DEBUG] Appending bind %s to app container", bind)
log.Printf("[DEBUG] Appending bind %s to App container", bind)
bindSplit := strings.Split(bind, ":")
sourceFolder := bindSplit[0]
destinationFolder := bindSplit[1]
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
readOnly := false
if len(bindSplit) > 2 {
mode := bindSplit[2]
if mode == "ro" {
readOnly = true
}
}
builtMount := mount.Mount{
Type: mount.TypeBind,
Source: sourceFolder,
Target: destinationFolder,
})
ReadOnly: readOnly,
}
hostConfig.Mounts = append(hostConfig.Mounts, builtMount)
}
}
@@ -954,7 +991,8 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
func cleanupKubernetesExecution(clientset *kubernetes.Clientset, workflowExecution shuffle.WorkflowExecution, namespace string) error {
// workerName := fmt.Sprintf("worker-%s", workflowExecution.ExecutionId)
labelSelector := fmt.Sprintf("app=shuffle-app,executionId=%s", workflowExecution.ExecutionId)
// FIXME: The executionId label is currently not set
labelSelector := fmt.Sprintf("app.kubernetes.io/name=shuffle-app,executionId=%s", workflowExecution.ExecutionId)
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector,
@@ -1035,17 +1073,24 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
err = cli.ContainerStart(ctx, cont.ID, container.StartOptions{})
if err != nil {
if strings.Contains(fmt.Sprintf("%s", err), "cannot join network") || strings.Contains(fmt.Sprintf("%s", err), "No such container") {
// Remove the "CREATED" one from the previous:
removeErr := cli.ContainerRemove(ctx, cont.ID, container.RemoveOptions{})
if removeErr != nil {
log.Printf("[ERROR] Failed to remove container %s: %s", cont.ID, removeErr)
}
log.Printf("[WARNING] Failed deploying App on first attempt: %s. Removing some HostConfig configs.", err)
parsedUuid := uuid.NewV4()
identifier = fmt.Sprintf("%s-%s-nonetwork", identifier, parsedUuid)
hostConfig = &container.HostConfig{
LogConfig: container.LogConfig{
Type: "json-file",
Config: map[string]string{
"max-size": "10m",
},
hostConfig.NetworkMode = container.NetworkMode("")
hostConfig.LogConfig = container.LogConfig{
Type: "json-file",
Config: map[string]string{
"max-size": "10m",
},
Resources: container.Resources{},
}
hostConfig.Resources = container.Resources{}
cont, err = cli.ContainerCreate(
context.Background(),
@@ -1067,12 +1112,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
return err
}
log.Printf("[DEBUG] Running secondary check without network with worker")
//log.Printf("[DEBUG] Running secondary check without network with worker")
err = cli.ContainerStart(ctx, cont.ID, container.StartOptions{})
}
if err != nil {
log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err)
log.Printf("[ERROR] Failed to start container (2) in runtime location %s: %s", environment, err)
cacheErr := shuffle.DeleteCache(ctx, actionExecId)
if cacheErr != nil {
@@ -1232,6 +1277,7 @@ func getWorkerURLs() ([]string, error) {
}
func askOtherWorkersToDownloadImage(image string) {
// Why wouldn't it happen on swarm? Hmm
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
return
}
@@ -1474,10 +1520,6 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
env = append(env, fmt.Sprintf("SHUFFLE_APP_SDK_TIMEOUT=%s", os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")))
}
// Fixes issue:
// standard_go init_linux.go:185: exec user process caused "argument list too long"
// https://devblogs.microsoft.com/oldnewthing/20100203-00/?p=15083
// FIXME: Ensure to NEVER do this anymore
// This potentially breaks too much stuff. Better to have the app poll the data.
_ = executionData
@@ -1501,9 +1543,11 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
fmt.Sprintf("%s:%s_%s", baseimagename, parsedAppname, action.AppVersion),
}
// If cleanup is set, it should run for efficiency
// This is the weirdest shit ever looking back at
// Needs optimization lol
pullOptions := image.PullOptions{}
if cleanupEnv == "true" {
if strings.ToLower(cleanupEnv) == "true" {
err = deployApp(dockercli, images[0], identifier, env, workflowExecution, action)
if err != nil && !strings.Contains(err.Error(), "Conflict. The container name") {
if strings.Contains(err.Error(), "exited prematurely") {