diff --git a/.env b/.env index 53204db2..0b1e8cee 100755 --- a/.env +++ b/.env @@ -1,5 +1,4 @@ # Default execution environment for workers -ORG_ID=Shuffle ENVIRONMENT_NAME=Shuffle # Sanitize liquid.py input diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5990d9c6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/nightly-release.yaml b/.github/workflows/dockerbuild-nightly.yaml similarity index 73% rename from .github/workflows/nightly-release.yaml rename to .github/workflows/dockerbuild-nightly.yaml index b66f9559..cb7907c6 100644 --- a/.github/workflows/nightly-release.yaml +++ b/.github/workflows/dockerbuild-nightly.yaml @@ -1,11 +1,15 @@ -name: Nightly Release -on: - release: - types: [published] - branches: - - main - - nightly +name: nightly-dockerbuild +on: + workflow_dispatch: + push: + branches: + - nightly + paths: + - "**" + - "!.github/**" + - "!**.md" + - "!docker-compose.yml" jobs: main: runs-on: ubuntu-latest @@ -16,28 +20,23 @@ jobs: 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 version - id: set_version - run: | - if [[ ${{ github.event_name }} == 'release' ]]; then - echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT - else - echo "VERSION=nightly-untagged-latest" >> $GITHUB_OUTPUT - fi - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -74,10 +73,10 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache tags: | - ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }} - ${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }} - frikky/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }} + 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 }} \ No newline at end of file + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/dockerbuild.yaml b/.github/workflows/dockerbuild.yaml index a04e87bd..c1e9640c 100644 --- a/.github/workflows/dockerbuild.yaml +++ b/.github/workflows/dockerbuild.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - nightly + - main paths: - "**" - "!.github/**" @@ -20,19 +20,19 @@ jobs: include: - app: frontend path: frontend - version: nightly + version: 2.0.2 experimental: true - app: backend path: backend - version: nightly + version: 2.0.2 experimental: true - app: orborus path: functions/onprem/orborus - version: nightly + version: 2.0.2 experimental: true - app: worker path: functions/onprem/worker - version: nightly + version: 2.0.2 experimental: true steps: - name: Checkout @@ -74,6 +74,7 @@ jobs: cache-to: type=local,dest=/tmp/.buildx-cache tags: | ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ matrix.version }} + ghcr.io/shuffle/shuffle-${{ matrix.app }}:latest ${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ matrix.version }} frikky/shuffle-${{ matrix.app }}:${{ matrix.version }} frikky/shuffle:${{ matrix.app }} diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml index e365080e..4c3c6b6f 100644 --- a/.github/workflows/helm-release.yml +++ b/.github/workflows/helm-release.yml @@ -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 diff --git a/.github/workflows/project_automation.yml b/.github/workflows/project_automation.yml deleted file mode 100644 index 0e3bf945..00000000 --- a/.github/workflows/project_automation.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Automation - Add all new issues to roadmap project - -on: - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/Shuffle/projects/8 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/quick-testing.yml b/.github/workflows/quick-testing.yml index 6bf468d0..cf35967a 100644 --- a/.github/workflows/quick-testing.yml +++ b/.github/workflows/quick-testing.yml @@ -14,16 +14,21 @@ jobs: matrix: os: [ubuntu-latest] architecture: [x64, arm64] - steps: - name: Checkout code uses: actions/checkout@v2 - + + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + docker-compose --version + - name: Set up opensearch directory run: chmod -R 777 shuffle-database - name: Build the stack - run: docker-compose up -d + run: docker compose up -d - name: Wait for 30 seconds run: sleep 30 @@ -55,7 +60,6 @@ jobs: echo "The word 'Shuffle' was not found in the response." exit 1 fi - - name: Register a user and check the status code run: | MAX_RETRIES=30 @@ -96,31 +100,36 @@ jobs: echo "User registration failed after $MAX_RETRIES attempts." exit 1 + - name: Run Selenium testing for frontend + run: | + cd $GITHUB_WORKSPACE/frontend + # write some log to see the current directory + chmod +x frontend-testing.sh + ./frontend-testing.sh + - name: Get the API key and run a health check + id: health_check run: | RESPONSE=$(curl -s -k -u admin:StrongShufflePassword321! 'https://localhost:9200/users/_search') + echo "Raw Response: $RESPONSE" + API_KEY=$(echo "$RESPONSE" | jq -r '.hits.hits[0]._source.apikey') - if [ -n "$API_KEY" ]; then + if [ -n "$API_KEY" ] && [ "$API_KEY" != "null" ]; then echo "Admin API key: $API_KEY" + echo "API_KEY=$API_KEY" >> $GITHUB_ENV else echo "Failed to retrieve the API key for the admin user." exit 1 fi + echo "Waiting 1 minute before sending the health API request..." + sleep 60 + + echo "Checking health API..." HEALTH_RESPONSE=$(curl -s 'http://localhost:3001/api/v1/health?force=true' \ - -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \ - -H 'Accept-Language: en-US,en;q=0.9' \ - -H 'Connection: keep-alive' \ - -H 'Sec-Fetch-Dest: document' \ - -H 'Sec-Fetch-Mode: navigate' \ - -H 'Sec-Fetch-Site: none' \ - -H "Authorization: Bearer $API_KEY" \ - -H 'Sec-Fetch-User: ?1' \ - -H 'Upgrade-Insecure-Requests: 1' \ - -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' \ - -H 'sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"' \ - -H 'sec-ch-ua-mobile: ?0' \ - -H 'sec-ch-ua-platform: "macOS"') + -H "Authorization: Bearer $API_KEY") + + echo "Health API Response: $HEALTH_RESPONSE" WORKFLOWS_CREATE=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.create') WORKFLOWS_RUN=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.run') @@ -128,9 +137,35 @@ jobs: WORKFLOWS_RUN_STATUS=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.run_status') WORKFLOWS_DELETE=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.delete') + echo "WORKFLOWS_CREATE: $WORKFLOWS_CREATE" + echo "WORKFLOWS_RUN: $WORKFLOWS_RUN" + echo "WORKFLOWS_RUN_FINISHED: $WORKFLOWS_RUN_FINISHED" + echo "WORKFLOWS_RUN_STATUS: $WORKFLOWS_RUN_STATUS" + echo "WORKFLOWS_DELETE: $WORKFLOWS_DELETE" + if [ "$WORKFLOWS_CREATE" = "true" ] && [ "$WORKFLOWS_RUN" = "true" ] && [ "$WORKFLOWS_RUN_FINISHED" = "true" ] && [ "$WORKFLOWS_RUN_STATUS" = "FINISHED" ] && [ "$WORKFLOWS_DELETE" = "true" ]; then echo "Health endpoint check was successful." + exit 0 else - echo "Health endpoint check failed. Response did not meet expected criteria." + echo "Health check failed." exit 1 fi + + notify: + needs: build + if: failure() + runs-on: ubuntu-latest + steps: + - name: Send Twilio Alert + run: | + WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + MESSAGE="🚨 Shuffle Workflow Failed! + Repository: ${{ github.repository }} + Branch: ${{ github.ref_name }} + Workflow URL: $WORKFLOW_URL" + + curl -s -X POST https://api.twilio.com/2010-04-01/Accounts/${{ secrets.TWILIO_ACCOUNT_SID }}/Messages.json \ + --data-urlencode "To=${{ secrets.TWILIO_TO_NUMBER }}" \ + --data-urlencode "From=${{ secrets.TWILIO_FROM_NUMBER }}" \ + --data-urlencode "Body=$MESSAGE" \ + -u "${{ secrets.TWILIO_ACCOUNT_SID }}:${{ secrets.TWILIO_AUTH_TOKEN }}" > /dev/null diff --git a/.github/workflows/snyk-container-analysis.yml b/.github/workflows/snyk-container-analysis.yml deleted file mode 100755 index f9a406b2..00000000 --- a/.github/workflows/snyk-container-analysis.yml +++ /dev/null @@ -1,50 +0,0 @@ -# A sample workflow which checks out the code, builds a container -# image using Docker and scans that image for vulnerabilities using -# Snyk. The results are then uploaded to GitHub Security Code Scanning -# -# For more examples, including how to limit scans to only high-severity -# issues, monitor images for newly disclosed vulnerabilities in Snyk and -# fail PR checks for new vulnerabilities, see https://github.com/snyk/actions/ - -name: Snyk Container - -on: - push: - branches: - - launch - pull_request: - # The branches below must be a subset of the branches above - branches: - - master - - launch - schedule: - - cron: '18 4 * * 3' - -jobs: - snyk: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build a Docker image - run: docker build -t frontend . - - - name: Run Snyk to check Docker image for vulnerabilities - # Snyk can be used to break the build when it detects vulnerabilities. - # In this case we want to upload the issues to GitHub Code Scanning - continue-on-error: true - uses: snyk/actions/docker@master - env: - # In order to use the Snyk Action you will need to have a Snyk API token. - # More details in https://github.com/snyk/actions#getting-your-snyk-token - # or you can signup for free at https://snyk.io/login - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - image: your/image-to-test - args: --file=Dockerfile - - - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: snyk.sarif diff --git a/.github/workflows/snyk-infrastructure-analysis.yml b/.github/workflows/snyk-infrastructure-analysis.yml deleted file mode 100755 index 4402487d..00000000 --- a/.github/workflows/snyk-infrastructure-analysis.yml +++ /dev/null @@ -1,46 +0,0 @@ -# A sample workflow which checks out your Infrastructure as Code Configuration files, -# such as Kubernetes, Helm & Terraform and scans them for any security issues. -# The results are then uploaded to GitHub Security Code Scanning -# -# For more examples, including how to limit scans to only high-severity issues -# and fail PR checks, see https://github.com/snyk/actions/ - -name: Snyk Infrastructure as Code - -on: - push: - branches: - - master - - launch - pull_request: - # The branches below must be a subset of the branches above - branches: - - master - - launch - schedule: - - cron: '41 16 * * 2' - -jobs: - snyk: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Run Snyk to check configuration files for security issues - # Snyk can be used to break the build when it detects security issues. - # In this case we want to upload the issues to GitHub Code Scanning - continue-on-error: true - uses: snyk/actions/iac@master - env: - # In order to use the Snyk Action you will need to have a Snyk API token. - # More details in https://github.com/snyk/actions#getting-your-snyk-token - # or you can signup for free at https://snyk.io/login - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - # Add the path to the configuration file that you would like to test. - # For example `deployment.yaml` for a Kubernetes deployment manifest - # or `main.tf` for a Terraform configuration file - file: your-file-to-test.yaml - - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: snyk.sarif diff --git a/backend/Dockerfile b/backend/Dockerfile index 7cab5e27..9e27ffbe 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22 as builder +FROM golang:1.24 as builder # Add files RUN mkdir /app @@ -14,9 +14,8 @@ ADD ./go-app/go.mod /app RUN wget -O /app_sdk/app_base.py https://raw.githubusercontent.com/Shuffle/app_sdk/refs/heads/main/shuffle_sdk/shuffle_sdk.py ADD ./app_gen /app_gen -RUN go get -v -#RUN go mod tidy -#RUN go clean -modcache +RUN go mod download +RUN go mod tidy # From November 2022, CGO is enabled due to packages # that we use requiring it. This is a temporary fix diff --git a/backend/app_gen/openapi/baseline/requirements.txt b/backend/app_gen/openapi/baseline/requirements.txt index dfad3eb9..c0d2f96f 100755 --- a/backend/app_gen/openapi/baseline/requirements.txt +++ b/backend/app_gen/openapi/baseline/requirements.txt @@ -1,3 +1,11 @@ # No extra requirements needed -requests -urllib3 +requests==2.32.3 +urllib3==2.3.0 +liquidpy==0.8.2 +MarkupSafe==3.0.2 +flask[async]==3.1.0 +python-dateutil==2.9.0.post0 +PyJWT==2.10.1 +cryptography==44.0.2 +shufflepy==0.1.0 +shuffle-sdk==0.0.25 diff --git a/backend/app_sdk/requirements.txt b/backend/app_sdk/requirements.txt deleted file mode 100644 index 2dfbed0d..00000000 --- a/backend/app_sdk/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -urllib3==1.26.19 -requests==2.31.0 -MarkupSafe==2.0.1 -liquidpy==0.8.1 -flask[async]==2.0.2 -waitress==2.1.0 -#flask==1.1.2 -python-dateutil==2.8.1 - diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 57d4aaf8..b7ee7e6d 100755 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -211,7 +211,7 @@ func fixTags(tags []string) []string { func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string, downloadIfFail bool) error { ctx := context.Background() client, err := client.NewEnvClient() - defer client.Close() + defer client.Close() if err != nil { log.Printf("Unable to create docker client: %s", err) return err @@ -473,73 +473,84 @@ func buildImage(tags []string, dockerfileLocation string) error { } } } - } else { - - ctx := context.Background() - client, err := client.NewEnvClient() - defer client.Close() - if err != nil { - log.Printf("Unable to create docker client: %s", err) - return err - } - - log.Printf("[INFO] Docker Tags: %s", tags) - dockerfileSplit := strings.Split(dockerfileLocation, "/") - - // Create a buffer - buf := new(bytes.Buffer) - tw := tar.NewWriter(buf) - defer tw.Close() - baseDir := strings.Join(dockerfileSplit[0:len(dockerfileSplit)-1], "/") - - // Builds the entire folder into buf - err = getParsedTar(tw, baseDir, "") - if err != nil { - log.Printf("Tar issue: %s", err) - } - - dockerFileTarReader := bytes.NewReader(buf.Bytes()) - buildOptions := types.ImageBuildOptions{ - Remove: true, - Tags: tags, - BuildArgs: map[string]*string{}, - } - //NetworkMode: "host", - - httpProxy := os.Getenv("HTTP_PROXY") - if len(httpProxy) > 0 { - buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy - } - httpsProxy := os.Getenv("HTTPS_PROXY") - if len(httpProxy) > 0 { - buildOptions.BuildArgs["https_proxy"] = &httpsProxy - } - - // Build the actual image - imageBuildResponse, err := client.ImageBuild( - ctx, - dockerFileTarReader, - buildOptions, - ) - - if err != nil { - return err - } - - // Read the STDOUT from the build process - defer imageBuildResponse.Body.Close() - buildBuf := new(strings.Builder) - _, err = io.Copy(buildBuf, imageBuildResponse.Body) - if err != nil { - return err - } else { - if strings.Contains(buildBuf.String(), "errorDetail") { - log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) - return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ","))) - } - } + return nil } + + ctx := context.Background() + client, err := client.NewEnvClient() + defer client.Close() + if err != nil { + log.Printf("Unable to create docker client: %s", err) + return err + } + + log.Printf("[INFO] Docker Tags: %s", tags) + dockerfileSplit := strings.Split(dockerfileLocation, "/") + + // Create a buffer + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + defer tw.Close() + baseDir := strings.Join(dockerfileSplit[0:len(dockerfileSplit)-1], "/") + + // Builds the entire folder into buf + err = getParsedTar(tw, baseDir, "") + if err != nil { + log.Printf("[ERROR] Tar issue during app build: %s", err) + } + + dockerFileTarReader := bytes.NewReader(buf.Bytes()) + buildOptions := types.ImageBuildOptions{ + Remove: true, + Tags: tags, + BuildArgs: map[string]*string{}, + } + //NetworkMode: "host", + + httpProxy := os.Getenv("HTTP_PROXY") + if len(httpProxy) > 0 { + buildOptions.BuildArgs["HTTP_PROXY"] = &httpProxy + } + httpsProxy := os.Getenv("HTTPS_PROXY") + if len(httpProxy) > 0 { + buildOptions.BuildArgs["https_proxy"] = &httpsProxy + } + + // Print the actual file content from dockerFileTarReader + /* + data, err := ioutil.ReadAll(dockerFileTarReader) + if err != nil { + log.Printf("[ERROR] Failed reading Dockerfile TAR reader: %s", err) + } else { + log.Printf("[DEBUG] Dockerfile TAR reader content: %s", string(data)) + } + */ + + // Build the actual image + imageBuildResponse, err := client.ImageBuild( + ctx, + dockerFileTarReader, + buildOptions, + ) + + if err != nil { + return err + } + + // Read the STDOUT from the build process + defer imageBuildResponse.Body.Close() + buildBuf := new(strings.Builder) + _, err = io.Copy(buildBuf, imageBuildResponse.Body) + if err != nil { + return err + } else { + if strings.Contains(buildBuf.String(), "errorDetail") { + log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) + return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ","))) + } + } + return nil } @@ -604,11 +615,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 +657,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, "") { + continue + } + if strings.ToLower(tag) == strings.ToLower(version.Name) { img = image tagFound = tag @@ -834,7 +881,7 @@ func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user type tmpapp struct { Success bool `json:"success"` OpenAPI string `json:"openapi"` - App string `json:"app"` + App string `json:"app"` } app := tmpapp{} @@ -984,11 +1031,4 @@ func activateWorkflowAppDocker(resp http.ResponseWriter, request *http.Request) log.Printf("[INFO] User %s (%s) is activating %s. Public: %t, Shared: %t", user.Username, user.Id, app.Name, app.Public, app.Sharing) buildSwaggerApp(resp, []byte(openApiApp.Body), user, true) - - //app.Active = true - //app.Generated = true - //app, err := shuffle.SetApp(ctx, app) - - //resp.WriteHeader(200) - //resp.Write([]byte(`{"success": true}`)) } diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 6b3c8108..aeb3e7e0 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -1,91 +1,109 @@ module shuffle -go 1.22.2 +go 1.24.0 +toolchain go1.24.3 + +//replace github.com/frikky/schemaless => ../../../schemaless //replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi + require ( - cloud.google.com/go/datastore v1.15.0 - cloud.google.com/go/storage v1.40.0 + cloud.google.com/go/datastore v1.20.0 + cloud.google.com/go/storage v1.55.0 github.com/basgys/goxml2json v1.1.0 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 - github.com/docker/docker v27.5.0+incompatible + github.com/docker/docker v28.2.2+incompatible github.com/frikky/kin-openapi v0.42.0 - github.com/fsouza/go-dockerclient v1.11.0 + github.com/fsouza/go-dockerclient v1.12.1 github.com/ghodss/yaml v1.0.0 - github.com/go-git/go-billy/v5 v5.6.0 - github.com/go-git/go-git/v5 v5.13.0 + github.com/go-git/go-billy/v5 v5.6.2 + github.com/go-git/go-git/v5 v5.16.1 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.0 - golang.org/x/crypto v0.31.0 - google.golang.org/api v0.176.1 - google.golang.org/grpc v1.68.1 - gopkg.in/src-d/go-git.v4 v4.13.1 + github.com/shuffle/shuffle-shared v0.8.84 + golang.org/x/crypto v0.38.0 + google.golang.org/api v0.236.0 + google.golang.org/grpc v1.72.2 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 - k8s.io/client-go v0.30.2 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 ) require ( - cloud.google.com/go v0.112.1 // indirect - cloud.google.com/go/auth v0.3.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect - cloud.google.com/go/iam v1.1.7 // indirect - cloud.google.com/go/scheduler v1.10.6 // indirect + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go v0.121.1 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/scheduler v1.11.7 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.1.3 // indirect - github.com/adrg/strutil v0.2.3 // indirect - github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/strutil v0.3.1 // indirect + github.com/algolia/algoliasearch-client-go/v3 v3.31.4 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/containerd v1.6.26 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/cyphar/filepath-securejoin v0.2.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/frikky/schemaless v0.0.13 // indirect + github.com/frikky/schemaless v0.0.16 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v28 v28.1.1 // indirect - github.com/google/go-querystring v1.0.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -93,51 +111,58 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opensearch-project/opensearch-go v1.1.0 // indirect github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sashabaranov/go-openai v1.19.2 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/sashabaranov/go-openai v1.40.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect - github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/src-d/gcfg v1.4.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.19.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 334170e4..098db5c0 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -7,58 +9,65 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= -cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/scheduler v1.10.6 h1:5U8iXLoQ03qOB+ZXlAecU7fiE33+u3QiM9nh4cd0eTE= -cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE= +cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM= +cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.9.10 h1:TxXGNmcbQxBKVWvjvTocNb6jrPyeHlk5EiDhhgHgggs= -github.com/Microsoft/hcsshim v0.9.10/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ= -github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1 h1:FP2Xtqqs/sefR5Qluygp+jVV+juXzEdJaPrZTCDLhDQ= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= +github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4 h1:UJhx6AhZCYf0qZygDz2c1x1+1q2q2sfzsRaQM6yswWk= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -81,107 +90,115 @@ github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUK github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw= -github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/containerd v1.6.26 h1:VVfrE6ZpyisvB1fzoY8Vkiq4sy+i5oF4uk7zu03RaHs= -github.com/containerd/containerd v1.6.26/go.mod h1:I4TRdsdoo5MlKob5khDJS2EPT1l1oMNaE2MBm6FrwxM= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= -github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= -github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= -github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0= github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= -github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ= -github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= -github.com/fsouza/go-dockerclient v1.11.0 h1:4ZAk6W7rPAtPXm7198EFqA5S68rwnNQORxlOA5OurCA= -github.com/fsouza/go-dockerclient v1.11.0/go.mod h1:0I3TQCRseuPTzqlY4Y3ajfsg2VAdMQoazrkxJTiJg8s= +github.com/frikky/schemaless v0.0.16 h1:4d2ZktB9xGsAusbbKliOI8TuriSrdIMzD/6ToY3wkz8= +github.com/frikky/schemaless v0.0.16/go.mod h1:jT48kTcmr1q3o8i+8qe7g+eCsbwaz2Q9CjOJevQQzQs= +github.com/fsouza/go-dockerclient v1.12.1 h1:FMoLq+Zhv9Oz/rFmu6JWkImfr6CBgZOPcL+bHW4gS0o= +github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-git/v5 v5.16.1 h1:TuxMBWNL7R05tXsUGi0kh1vi4tq0WfXNLlIrAkXG1k8= +github.com/go-git/go-git/v5 v5.16.1/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -190,74 +207,61 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -266,19 +270,17 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -286,19 +288,20 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -310,59 +313,61 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= -github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU= +github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs= +github.com/sendgrid/sendgrid-go v3.16.1+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/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM= +github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk= +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= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -372,63 +377,59 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= -go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= -go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= -go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -437,8 +438,6 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -458,8 +457,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -473,27 +470,23 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -502,11 +495,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -518,7 +510,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -530,14 +521,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -548,12 +539,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -565,9 +556,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -583,14 +572,12 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -599,8 +586,8 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= -google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= +google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0= +google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -621,95 +608,72 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= -k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= -k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 06412fb4..fdd9dc05 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -11,17 +11,18 @@ import ( "crypto/md5" "strconv" + "os" + "io" + "log" + "fmt" + "errors" + "net/url" + "os/exec" + "net/http" + "io/ioutil" + "math/rand" "encoding/hex" "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "os/exec" "net/http/httptest" "strings" @@ -31,11 +32,12 @@ import ( "github.com/frikky/kin-openapi/openapi2conv" "github.com/frikky/kin-openapi/openapi3" - //"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" gitProxy "github.com/go-git/go-git/v5/plumbing/transport" + http2 "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" // Random @@ -46,7 +48,8 @@ import ( // Web "github.com/gorilla/mux" - http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + //http2 "gopkg.in/src-d/go-git.v5/plumbing/transport/http" + //http2 "github.com/go-git/go-git/plumbing/transport/http" ) // This is used to handle onprem vs offprem databases etc @@ -59,6 +62,7 @@ var registryName = "registry.hub.docker.com" var runningEnvironment = "onprem" var syncUrl = "https://shuffler.io" +//var syncUrl = "http://localhost:5002" type retStruct struct { Success bool `json:"success"` @@ -445,7 +449,7 @@ func checkGitProxy(cloneOptions *git.CloneOptions) *git.CloneOptions { func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) error { // Returns false if there is an issue // Use this for register - err := shuffle.CheckPasswordStrength(password) + err := shuffle.CheckPasswordStrength(username, password) if err != nil { log.Printf("[WARNING] Bad password strength: %s", err) return err @@ -458,8 +462,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) } ctx := context.Background() - //users, err := FindUser(ctx context.Context, username string) ([]User, error) { - users, err := shuffle.FindUser(ctx, strings.ToLower(strings.TrimSpace(username))) if err != nil && len(users) == 0 { log.Printf("[WARNING] Failed getting user %s: %s", username, err) @@ -484,7 +486,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) newUser.Active = true newUser.Orgs = []string{org.Id} - // FIXME - Remove this later if role == "admin" { newUser.Role = "admin" newUser.Roles = []string{"admin"} @@ -637,7 +638,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { } else { log.Printf("[DEBUG] Successfully created the default org!") - defaultEnv := os.Getenv("ORG_ID") + defaultEnv := os.Getenv("ENVIRONMENT_NAME") if len(defaultEnv) == 0 { defaultEnv = "Shuffle" log.Printf("[DEBUG] Setting default environment for org to %s", defaultEnv) @@ -1022,6 +1023,11 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { } } + if parsedAdmin == "true" { + userInfo.Role = "admin" + userInfo.ActiveOrg.Role = "admin" + } + chatDisabled := false if os.Getenv("SHUFFLE_CHAT_DISABLED") == "true" { chatDisabled = true @@ -1084,6 +1090,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { Priorities: orgPriorities, Licensed: licensed, ActiveApps: activatedAppIds, + Theme: userInfo.Theme, } returnData, err := json.Marshal(returnValue) @@ -1844,6 +1851,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // return //} + log.Printf("[DEBUG] HOOKS: webhook callback: %s", request.URL.String()) + if request.Method != "POST" { request.Method = "POST" } @@ -1855,6 +1864,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { path := strings.Split(request.URL.String(), "/") if len(path) < 4 { + log.Printf("[DEBUG] HOOKS: Invalid webhook path: %s", request.URL.String()) resp.WriteHeader(403) resp.Write([]byte(`{"success": false}`)) return @@ -1870,7 +1880,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { if location[1] == "api" { if len(location) <= 4 { log.Printf("[INFO] Couldn't handle location. Too short in webhook: %d", len(location)) - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } @@ -1887,6 +1897,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } } + log.Printf("[DEBUG] HOOKS: Pre user agent check") + // Find user agent header userAgent := request.Header.Get("User-Agent") if strings.Contains(strings.ToLower(userAgent), "microsoftpreview") || strings.Contains(strings.ToLower(userAgent), "googlebot") { @@ -1909,8 +1921,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //log.Printf("HookID: %s", hookId) hook, err := shuffle.GetHook(ctx, hookId) if err != nil { - log.Printf("[WARNING] Failed getting hook %s (callback): %s", hookId, err) - resp.WriteHeader(401) + log.Printf("[WARNING] HOOKS: Failed getting hook %s (callback): %s", hookId, err) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } @@ -1922,21 +1934,21 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //resp.WriteHeader(200) //resp.Write([]byte(`{"success": true}`)) if hook.Status == "stopped" { - log.Printf("[WARNING] Not running %s because hook status is stopped", hook.Id) - resp.WriteHeader(401) + log.Printf("[WARNING] HOOKS: Not running %s because hook status is stopped", hook.Id) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Is it running?"}`))) return } if len(hook.Workflows) == 0 { - log.Printf("[DEBUG] Not running because hook isn't connected to any workflows") - resp.WriteHeader(401) + log.Printf("[DEBUG] HOOKS: Not running because hook isn't connected to any workflows") + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflows are defined"}`))) return } if hook.Environment == "cloud" { - log.Printf("[DEBUG] This should trigger in the cloud. Duplicate action allowed onprem.") + log.Printf("[DEBUG] HOOKS: This should trigger in the cloud. Duplicate action allowed onprem.") } // Check auth @@ -1952,7 +1964,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadAll(request.Body) if err != nil { - log.Printf("[DEBUG] Body data error: %s", err) + log.Printf("[DEBUG] HOOKS: data read error: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -1993,7 +2005,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { b, err := json.Marshal(newBody) if err != nil { - log.Printf("[ERROR] Failed newBody marshaling for webhook: %s", err) + log.Printf("[ERROR] HOOKS: Failed newBody marshaling for webhook: %s", err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return @@ -2009,7 +2021,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } if len(hook.Start) == 0 { - log.Printf("[WARNING] No start node for hook %s - running with workflow default.", hook.Id) + log.Printf("[ERROR] HOOKS: No start node for hook %s - running with workflow default.", hook.Id) //bodyWrapper = string(parsedBody) } @@ -2021,7 +2033,6 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest, hook.OrgId) - if err == nil { if hook.Version == "v2" { timeout := 15 @@ -2056,6 +2067,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } else { resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s"}`, workflowExecution.ExecutionId))) } + return } @@ -2063,6 +2075,10 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) } + log.Printf("[ERROR] HOOKS: END OF FUNCTION FOR '%s'. IF this is reached, something went wrong.", hook.Id) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Failed to run workflow. Check logs."}`)) + } func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) { @@ -3079,8 +3095,9 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s return } - // FIXME: Check whether it's in use. - if user.Id != app.Owner && user.Role != "admin" { + if user.Id == app.Owner || (user.Role == "admin" && user.ActiveOrg.Id == app.ReferenceOrg) || shuffle.ArrayContains(app.Contributors, user.Id) { + log.Printf("[DEBUG] Editing app %s with user %s (%s) in org %s", test.Id, user.Username, user.Id, user.ActiveOrg.Id) + } else { log.Printf("[WARNING] Wrong user (%s) for app %s when verifying swagger", user.Username, app.Name) resp.WriteHeader(403) resp.Write([]byte(`{"success": false, "reason": "You don't have permissions to edit this app. Contact support@shuffler.io if this persists."}`)) @@ -3154,7 +3171,18 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s } } - api.Owner = user.Id + if api.Owner == "" { + api.Owner = user.Id + } + + if len(api.ReferenceOrg) == 0 { + api.ReferenceOrg = user.ActiveOrg.Id + } + + if len(test.Image) > 0 { + api.SmallImage = test.Image + api.LargeImage = test.Image + } err = shuffle.DumpApi(basePath, api) if err != nil { @@ -3279,6 +3307,12 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s Body: string(body), } + if !shuffle.ArrayContains(api.Contributors, user.Id) { + api.Contributors = append(api.Contributors, user.Id) + } + + shuffle.SetAppRevision(ctx, api) + log.Printf("[INFO] API LENGTH FOR %s: %d, ID: %s", api.Name, len(parsed.Body), newmd5) // FIXME: Might cause versioning issues if we re-use the same!! // FIXME: Need a way to track different versions of the same app properly. @@ -3354,6 +3388,21 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "id": "%s"}`, api.ID))) } + + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[ERROR] Failed getting org during image build (%s): %s", user.ActiveOrg.Id, err) + } else { + imagenames := []string{ + fmt.Sprintf("%s_%s", api.Name, api.AppVersion), + fmt.Sprintf("%s_%s", api.Name, api.ID), + } + + err = shuffle.DistributeAppToEnvironments(ctx, *org, imagenames) + if err != nil { + log.Printf("[ERROR] Failed distributing app to environments: %s", err) + } + } } // Creates an app from the app builder @@ -3507,7 +3556,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 { @@ -3752,30 +3806,57 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { } } - if org.SyncConfig.WorkflowBackup { - workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") - if err != nil { - log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) - } else { - backupJob.Workflows = workflows - } + // Check if it's 1/20 times (600 seconds - 10 min on average) + // Only problem: May take time to sync the first time, which is annoying + // This is to ensure that we don't spam the shuffle cloud servers with a lot of data + shouldBackupData := false + randomNumber := rand.Intn(20) + if randomNumber == 0 { + shouldBackupData = true } - if org.SyncConfig.AppBackup && len(org.Users) > 0 { - - apps, err := shuffle.GetPrioritizedApps(ctx, foundUser) - if err != nil { - log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err) - } else { - backupJob.Apps = apps + // Just to prevent it from spamming large outbound requests + if shouldBackupData { + if org.SyncConfig.WorkflowBackup { + workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "") + if err != nil { + log.Printf("[ERROR] Failed getting backup workflows for org %s: %s", org.Id, err) + } else { + backupJob.Workflows = workflows + } } - } - info, err := shuffle.GetOrgStatistics(ctx, org.Id) - if err != nil { - log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err) - } else { - backupJob.Stats = *info + if org.SyncConfig.AppBackup && len(org.Users) > 0 { + foundUser.ActiveOrg.Id = org.Id + apps, err := shuffle.GetPrioritizedApps(ctx, foundUser) + if err != nil { + log.Printf("[ERROR] Failed getting backup apps for org %s: %s", org.Id, err) + } else { + parsedApps := []shuffle.WorkflowApp{} + for _, app := range apps { + if len(app.Actions) == 0 { + continue + } + + if !app.Generated { + continue + } + + parsedApps = append(parsedApps, app) + } + + backupJob.Apps = parsedApps + } + } + + // Send stats once every 10 times or so..? + // For now, just send every time + info, err := shuffle.GetOrgStatistics(ctx, org.Id) + if err != nil { + log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err) + } else { + backupJob.Stats = *info + } } backupJobData, err := json.Marshal(backupJob) @@ -3812,6 +3893,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { //log.Printf("[ERROR] Failed cloud sync job controller run for '%s': %s", respBody, err) return err } + return nil } @@ -3843,7 +3925,7 @@ func runInitEs(ctx context.Context) { log.Printf("[INFO] Running with HTTPS proxy %s (env: HTTPS_PROXY)", httpsProxy) } - defaultEnv := os.Getenv("ORG_ID") + defaultEnv := os.Getenv("ENVIRONMENT_NAME") if len(defaultEnv) == 0 { defaultEnv = "Shuffle" log.Printf("[DEBUG] Setting default environment for org to %s", defaultEnv) @@ -3949,6 +4031,8 @@ func runInitEs(ctx context.Context) { time.Sleep(30 * time.Second) } + // FIXME: This should ONLY run on one backend instance + schedules, err := shuffle.GetAllSchedules(ctx, "ALL") if err != nil { log.Printf("[WARNING] Failed getting schedules during service init: %s", err) @@ -4092,7 +4176,7 @@ func runInitEs(ctx context.Context) { } //interval := int(org.SyncConfig.Interval) - interval := 15 + interval := 30 if interval == 0 { log.Printf("[WARNING] Skipping org %s because sync isn't set (0).", org.Id) continue @@ -4161,20 +4245,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) @@ -4188,14 +4278,22 @@ func runInitEs(ctx context.Context) { continue } + respBody, err := ioutil.ReadAll(newresp.Body) if err != nil { - log.Printf("[ERROR] Failed setting respbody %s", err) + log.Printf("[ERROR] Failed setting respbody %s for execution stop. Status: %d", err, newresp.StatusCode) continue } - 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) + if newresp.StatusCode != 200 { + if !strings.Contains(string(respBody), "is active") { + log.Printf("[WARNING] Failed stopping runs in environment %s. Status code: %d. Body: %s", environment, newresp.StatusCode, string(respBody)) + } + + continue + } + + url = fmt.Sprintf("http://localhost:%s/api/v1/environments/%s/rerun", backendPort, environment) req, err = http.NewRequest( "GET", url, @@ -4215,12 +4313,17 @@ func runInitEs(ctx context.Context) { continue } - respBody, err = ioutil.ReadAll(newresp.Body) - if err != nil { - log.Printf("[ERROR] Failed setting respbody %s", err) - continue + if newresp.StatusCode != 200 { + log.Printf("[WARNING] Failed rerunning environment %s. Status code: %d", environment, newresp.StatusCode) } - log.Printf("[DEBUG] Successfully ran workflow RERUN request for %s. Body: %s", environment, string(respBody)) + + //respBody, err := ioutil.ReadAll(newresp.Body) + //if err != nil { + // log.Printf("[ERROR] Failed setting respbody %s", err) + // continue + //} + + //log.Printf("[DEBUG] Ran workflow RERUN request for %s with the response. Body: %s", environment, string(respBody)) } } } @@ -4313,45 +4416,16 @@ func runInitEs(ctx context.Context) { log.Printf("[DEBUG] Skipping download of default apps as %d were found", len(workflowapps)) } - log.Printf("[INFO] Downloading OpenAPI data for search - EXTRA APPS") - apis := "https://github.com/shuffle/security-openapis" - - // THis gets memory problems hahah - //apis := "https://github.com/APIs-guru/openapi-directory" - fs := memfs.New() - storer := memory.NewStorage() - cloneOptions := &git.CloneOptions{ - URL: apis, - } - - cloneOptions = checkGitProxy(cloneOptions) - - _, err = git.Clone(storer, fs, cloneOptions) - if err != nil { - log.Printf("[ERROR] Failed loading repo %s into memory: %s", apis, err) - } else if err == nil && len(workflowapps) < 10 { - log.Printf("[INFO] Finished git clone. Looking for updates to the repo.") - dir, err := fs.ReadDir("") - if err != nil { - log.Printf("Failed reading folder: %s", err) - } - - iterateOpenApiGithub(fs, dir, "", "") - log.Printf("[INFO] Finished downloading extra API samples") - } else { - log.Printf("[INFO] Skipping download of extra API samples as %d were found", len(workflowapps)) - } - if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" { - healthcheckInterval := 30 - log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) + healthcheckInterval := 60 + log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats, and dashboard on /health. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) job := func() { // Prepare a fake http.responsewriter resp := httptest.NewRecorder() request := http.Request{} // Add the "force=true" query to the fake request - request.URL, err = url.Parse("/api/v1/health/stats?force=true") + request.URL, err = url.Parse("/api/v1/health?force=true") if err != nil { log.Printf("[ERROR] Failed to parse test url for healthstats: %s", err) } @@ -4422,7 +4496,7 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error) return &org, errors.New(fmt.Sprintf("Couldn't find any sync key to disable org %s", org.Id)) } - log.Printf("[INFO] Should run cloud sync disable for org %s with URL %s and sync key %s", org.Id, syncUrl, org.SyncConfig.Apikey) + log.Printf("[INFO] Should run cloud sync disable for org %s with URL %s", org.Id, syncUrl) client := shuffle.GetExternalClient(syncUrl) req, err := http.NewRequest( @@ -4635,7 +4709,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // If you want to disable cloud sync, see previous section. if org.CloudSync { log.Printf("[WARNING] Org %s is already syncing. Skip", org.Id) - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Your org is already syncing. Nothing to set up."}`))) return } @@ -4712,6 +4786,9 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { org.SyncConfig = shuffle.SyncConfig{ Apikey: responseData.SessionKey, Interval: responseData.IntervalSeconds, + + WorkflowBackup: true, + AppBackup: true, } interval := int(responseData.IntervalSeconds) @@ -5092,8 +5169,8 @@ func initHandlers() { // Changed from workflows/streams to streams, as appengine was messing up // This does not increase the API counter // Used by frontend - r.HandleFunc("/api/v1/streams", handleWorkflowQueue).Methods("POST") - r.HandleFunc("/api/v1/streams/results", handleGetStreamResults).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/streams", handleSetWorkflowExecution).Methods("POST") + r.HandleFunc("/api/v1/streams/results", handleGetWorkflowExecutionResult).Methods("POST", "OPTIONS") // Used by orborus r.HandleFunc("/api/v1/workflows/queue", handleGetWorkflowqueue).Methods("GET", "POST") @@ -5104,7 +5181,7 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/{key}/execute", executeSingleAction).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/{key}/run", executeSingleAction).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/categories", shuffle.GetActiveCategories).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}/activate", activateWorkflowAppDocker).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}/deactivate", activateWorkflowAppDocker).Methods("GET", "OPTIONS") @@ -5185,6 +5262,16 @@ func initHandlers() { r.HandleFunc("/api/v1/hooks/{key}/delete", shuffle.HandleDeleteHook).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/hooks/{key}", shuffle.HandleDeleteHook).Methods("DELETE", "OPTIONS") + // This structure is horrendous. Needs fixing after we got the prototype up + r.HandleFunc("/api/v1/detections", shuffle.HandleListDetectionCategories).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/detections/{detectionType}/connect", shuffle.HandleDetectionAutoConnect).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/detections/{detection_type}", shuffle.HandleGetDetectionRules).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/detections/{detection_type}/selected_rules/{action}", shuffle.HandleFolderToggle).Methods("PUT", "OPTIONS") + + r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules", shuffle.HandleGetSelectedRules).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules/save", shuffle.HandleSaveSelectedRules).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/detections/{detection_type}/{fileId}/{action}", shuffle.HandleToggleRule).Methods("PUT", "OPTIONS") + // OpenAPI configuration r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/verify_openapi", verifySwagger).Methods("POST", "OPTIONS") @@ -5327,6 +5414,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)) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index df7addb8..0f078dca 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -11,7 +11,7 @@ import ( "io" "io/ioutil" "log" - "math/rand" + //"math/rand" "net/http" "net/url" "os" @@ -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" @@ -33,15 +30,9 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/storage/memory" - http2 "gopkg.in/src-d/go-git.v4/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" + "github.com/go-git/go-git/v5/plumbing" + http2 "github.com/go-git/go-git/v5/plumbing/transport/http" gyaml "github.com/ghodss/yaml" ) @@ -206,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) @@ -255,17 +246,17 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { } // This is really the environment's name - NOT org-id - orgId := request.Header.Get("Org-Id") - if len(orgId) == 0 { + environment := request.Header.Get("Org-Id") + if len(environment) == 0 { log.Printf("[AUDIT] No org-id header set") resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Specify the org-id header."}`))) return } - environment := request.Header.Get("org") - if len(environment) == 0 { - //log.Printf("[AUDIT] No 'org' header set (get workflow queue). ") + orgId := request.Header.Get("Org") + if len(orgId) == 0 { + log.Printf("[AUDIT] No 'org' header set (get workflow queue). ") /* resp.WriteHeader(403) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Specify the org header. This can be done by setting the 'ORG' environment variable for Orborus to your Org ID in Shuffle"}`))) @@ -274,7 +265,8 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { } orborusLabel := request.Header.Get("x-orborus-label") - + _ = orborusLabel + // This section is cloud custom for now auth := request.Header.Get("Authorization") if len(auth) == 0 { @@ -289,211 +281,46 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { //log.Printf("[AUDIT] Get workflow queue for org %s, env %s, orborus label %s", orgId, environment, orborusLabel) ctx := shuffle.GetContext(request) - env, err := shuffle.GetEnvironment(ctx, orgId, "") - timeNow := time.Now().Unix() - if err == nil && len(env.Id) > 0 && len(env.Name) > 0 && request.Method == "POST" { - // Updates every 60 seconds~ - if time.Now().Unix() > env.Edited+60 { - env.RunningIp = shuffle.GetRequestIp(request) + envs, err := shuffle.GetEnvironments(ctx, orgId) + if err != nil || len(envs) == 0 { + log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", environment, err) + } - // Orborus label = custom label for Orborus - if len(orborusLabel) > 0 { - env.RunningIp = orborusLabel - } - - // Set the checkin cache - - - body, err := ioutil.ReadAll(request.Body) - if err == nil { - var envData shuffle.OrborusStats - err = json.Unmarshal(body, &envData) - if err == nil { - envData.RunningIp = env.RunningIp - - marshalled, err := json.Marshal(envData) - if err == nil { - cacheKey := fmt.Sprintf("queueconfig-%s-%s", env.Name, env.OrgId) - go shuffle.SetCache(context.Background(), cacheKey, marshalled, 2) - } - - - - if envData.Swarm { - env.Licensed = true - env.RunType = "docker" - } - - if envData.Kubernetes { - env.RunType = "k8s" - } - - envData.DataLake = env.DataLake - } - } - - env.Checkin = timeNow - err = shuffle.SetEnvironment(ctx, env) - if err != nil { - log.Printf("[ERROR] Failed updating environment: %s", err) - } + var env *shuffle.Environment + found := false + for i := range envs { + if envs[i].Name == environment { + env = &envs[i] + found = true + break } } + if !found { + log.Printf("[ERROR] Failed to find environment(%s) for org(%s)", environment, orgId) + resp.WriteHeader(400) + resp.Write([]byte(`{"success":false,"reason":"environment not found"}`)) + return + } + + timeNow := time.Now().Unix() + err = shuffle.HandleOrborusFailover(ctx, request, resp, env) + if err != nil { + if !strings.Contains(err.Error(), "mismatch") { + log.Printf("[WARNING] Failed handling Orborus failover: %s", err) + } + + log.Printf("[DEBUG] Issue with environment ID: %s", environment) + + return + } + //log.Printf("Found env: %#v", env) if len(env.OrgId) > 0 { - environment = env.OrgId + orgId = env.OrgId } - // FIXME: Workflow stats disabled for now - // as it caused too many problems - // goal: track docker stuff once a minute and graph it - // For now: Disable this as it caused too many problems - if request.Method == "POST" && true == false { - //log.Printf("[DEBUG] POST to workflowqueue") - if rand.Intn(10) == 0 { - // Parse out body - body, err := ioutil.ReadAll(request.Body) - if err == nil { - - // Parse out CPU, memory and disk. - - var envData shuffle.OrborusStats - err = json.Unmarshal(body, &envData) - if err == nil && !envData.Swarm && !envData.Kubernetes && (envData.CPU > 0 || envData.Memory > 0 || envData.Disk > 0) { - - // Set the input in memory - envData.OrgId = orgId - envData.Environment = environment - envData.OrborusLabel = orborusLabel - envData.Timestamp = time.Now().Unix() - - if envData.CPU > 0 && envData.MaxCPU > 0 { - envData.CPUPercent = float64(envData.CPU) / float64(envData.MaxCPU) - } - - if envData.Memory > 0 && envData.MaxMemory > 0 { - envData.MemoryPercent = float64(envData.Memory) / float64(envData.MaxMemory) - } - - // Check if CPU percent constantly has stayed above X% for the last Y requests - percentageCheck := 90 - concurrentChecks := 2 - - //if int(envData.CPUPercent) > percentageCheck { - // Get cached data - percentages := []float64{} - cacheKey := fmt.Sprintf("%s_%s_percent", orgId, strings.ToLower(environment)) - - // Marshal float list into []byte - cacheData := []byte{} - cache, err := shuffle.GetCache(ctx, cacheKey) - if err == nil { - // Unmarshal into percentages - cacheData := []byte(cache.([]uint8)) - err = json.Unmarshal(cacheData, &percentages) - if err != nil { - log.Printf("[INFO] error in cache unmarshal for percentages: %s", err) - } - - if len(percentages) > concurrentChecks { - percentages = percentages[:concurrentChecks] - } - - percentages = append(percentages, envData.CPUPercent) - if len(percentages) > concurrentChecks { - //log.Printf("[INFO] Checking percentages: %v", percentages) - - // percentageCheck := 1 - sendAlert := true - for _, p := range percentages { - if int(p) < percentageCheck { - //log.Printf("[AUDIT] CPU percent is below %d: %d", percentageCheck, int(p)) - sendAlert = false - break - } - } - - if sendAlert { - log.Printf("[INFO] CPU percent has been above %d percent for the last 5 requests. Sending alert. Env: %s, org: %s", percentageCheck, environment, orgId) - - // Set notification + alert for organization - err = shuffle.CreateOrgNotification( - ctx, - fmt.Sprintf("CPU percent has been above %d percent", percentageCheck), - fmt.Sprintf("A environment %s has been using more than %d\\% CPU for the last 5 requests.", environment, percentageCheck), - fmt.Sprintf("/admin?tab=environments"), - environment, - true, - ) - - if err != nil { - log.Printf("[ERROR] error creating notification: %s", err) - } - - org, err := shuffle.GetOrg(ctx, environment) - if err == nil { - foundRecommendation := false - for _, recommendation := range org.Priorities { - if strings.Contains(recommendation.Name, "CPU") { - foundRecommendation = true - break - } - } - - if !foundRecommendation { - // Add to start of org.Priorities - org, _ = shuffle.AddPriority(*org, shuffle.Priority{ - Name: fmt.Sprintf("High CPU in environment %s", orgId), - Description: fmt.Sprintf("The environment %s has been using more than %d percent CPU. This indicates you may need to look at scaling.", orgId, percentageCheck), - Type: "scale", - Active: true, - URL: fmt.Sprintf("/admin?tab=environments"), - Severity: 1, - }, false) - - //Make last item the first item - org.Priorities = append([]shuffle.Priority{org.Priorities[len(org.Priorities)-1]}, org.Priorities[:len(org.Priorities)-1]...) - err = shuffle.SetOrg(ctx, *org, org.Id) - if err != nil { - log.Printf("[ERROR] Problem setting org: %s", err) - } - } - } - } - - if len(percentages) > 1 { - percentages = percentages[1:] - } - } - - // Marshal float list into []byte - } else { - //log.Printf("[ERROR] Failed getting cache: %s", err) - percentages = append(percentages, envData.CPUPercent) - } - - if len(percentages) > 0 { - //log.Printf("[DEBUG] Setting cache for %s: %#v", cacheKey, percentages) - cacheData, err = json.Marshal(percentages) - if err != nil { - log.Printf("[INFO] error in cache marshal: %s", err) - } - - // Add the new data - go shuffle.SetCache(ctx, cacheKey, cacheData, 5) - } - } - - //log.Printf("CPU percent: %f", envData.CPUPercent) - //log.Printf("Memory percent: %f", envData.MemoryPercent*100) - - go shuffle.SetenvStats(ctx, envData) - } - } - } - - executionRequests, err := shuffle.GetWorkflowQueue(ctx, orgId, 100) + executionRequests, err := shuffle.GetWorkflowQueue(ctx, environment, 100) if err != nil { // Skipping as this comes up over and over //log.Printf("(2) Failed reading body for workflowqueue: %s", err) @@ -521,10 +348,10 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { } } - if len(orgId) > 0 { - env, err := shuffle.GetEnvironment(ctx, orgId, foundId) + if len(environment) > 0 { + env, err := shuffle.GetEnvironment(ctx, environment, foundId) if err != nil { - log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", orgId, err) + log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", environment, err) //resp.WriteHeader(401) //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No env found matching %s"}`, id))) //return @@ -557,7 +384,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } -func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { +func handleGetWorkflowExecutionResult(resp http.ResponseWriter, request *http.Request) { cors := shuffle.HandleCors(resp, request) if cors { return @@ -688,7 +515,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { } -func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { +func handleSetWorkflowExecution(resp http.ResponseWriter, request *http.Request) { cors := shuffle.HandleCors(resp, request) if cors { return @@ -707,19 +534,26 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { return } - //log.Printf("Actionresult unmarshal: %s", string(body)) - //log.Printf("[DEBUG] Got workflow result from %s of length %d", request.RemoteAddr, len(body)) - ctx := context.Background() - err = shuffle.ValidateNewWorkerExecution(ctx, body) - if err == nil { - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "success"}`))) - return - } else { - log.Printf("[DEBUG] Handling other execution variant (subflow?): %s", err) + // Allows override of existing executions. + // This is a way to set them back to 0 results and rerun the + // exact same. Primarily in use for Worker testing of specific workflows. + shouldReset := false + resetString, ok := request.URL.Query()["reset"] + if ok && len(resetString) > 0 { + if resetString[0] == "true" { + shouldReset = true + } } - //log.Printf("[DEBUG] Got workflow result from %s of length %d.", request.RemoteAddr, len(body)) + ctx := context.Background() + err = shuffle.ValidateNewWorkerExecution(ctx, body, shouldReset) + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully updated the execution"}`))) + return + } else { + //log.Printf("[DEBUG] Handling other execution variant (subflow?): %s", err) + } var actionResult shuffle.ActionResult err = json.Unmarshal(body, &actionResult) @@ -730,8 +564,6 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { //return } - //log.Printf("Received action: %#v", actionResult) - // 1. Get the WorkflowExecution(ExecutionId) from the database // 2. if ActionResult.Authentication != WorkflowExecution.Authentication -> exit // 3. Add to and update actionResult in workflowExecution @@ -788,7 +620,6 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl return } - //log.Printf("BASE LENGTH: %d", len(workflowExecution.Results)) workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false, 0) if err != nil { b, suberr := json.Marshal(actionResult) @@ -798,13 +629,12 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl log.Printf("[ERROR] Failed running of parsedexecution: %s. Data: %s", err, string(b)) } - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed updating execution"}`))) return } _ = dbSave - //resultLength := len(workflowExecution.Results) setExecution := true if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) @@ -983,7 +813,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 { @@ -1021,7 +851,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { } } - log.Printf("[DEBUG] Attempting to delete the workflow %s from the database...", fileId) + //log.Printf("[DEBUG] Attempting to delete the workflow %s from the database...", fileId) err = shuffle.DeleteKey(ctx, "workflow", fileId) if err != nil { log.Printf("[DEBUG] Failed deleting workflow key %s", fileId) @@ -1032,10 +862,13 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Should have deleted workflow %s (%s)", workflow.Name, fileId) - cacheKey := fmt.Sprintf("%s_workflows", user.Id) - shuffle.DeleteCache(ctx, cacheKey) + shuffle.DeleteCache(ctx, fmt.Sprintf("%s_workflows", user.Id)) shuffle.DeleteCache(ctx, fmt.Sprintf("%s_workflows", user.ActiveOrg.Id)) - log.Printf("[DEBUG] Cleared workflow cache for %s (%s)", user.Username, user.Id) + shuffle.DeleteCache(ctx, fmt.Sprintf("%s_%s", user.Username, fileId)) + shuffle.DeleteCache(ctx, fmt.Sprintf("workflow_%s_childworkflows", workflow.ID)) + if len(workflow.ParentWorkflowId) > 0 { + shuffle.DeleteCache(ctx, fmt.Sprintf("workflow_%s_childworkflows", workflow.ParentWorkflowId)) + } resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) @@ -1050,7 +883,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request ctx := context.Background() if workflow.ID == "" || workflow.ID != id { - tmpworkflow, err := shuffle.GetWorkflow(ctx, id) + tmpworkflow, err := shuffle.GetWorkflow(ctx, id, true) if err != nil { //log.Printf("[WARNING] Failed getting the workflow locally (execution setup): %s", err) return shuffle.WorkflowExecution{}, "Failed getting workflow", err @@ -1094,10 +927,12 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request workflow.Errors = []string{} } + /* if !workflow.IsValid { log.Printf("[ERROR] Stopped execution as workflow %s is not valid.", workflow.ID) return shuffle.WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow") } + */ maxExecutionDepth := 10 if os.Getenv("SHUFFLE_MAX_EXECUTION_DEPTH") != "" { @@ -1133,507 +968,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) @@ -1644,7 +978,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 { @@ -1897,7 +1230,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Inside execute workflow for ID %s", fileId) ctx := context.Background() - workflow, err := shuffle.GetWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId, true) if err != nil && workflow.ID == "" { log.Printf("[WARNING] Failed getting the workflow locally (execute workflow): %s", err) resp.WriteHeader(401) @@ -1954,6 +1287,20 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) + + // Check for "wait" query if it's true + wait, waitok := request.URL.Query()["wait"] + if waitok && wait[0] == "true" { + returnBody := shuffle.HandleRetValidation(ctx, workflowExecution, 1) + returnBytes, err := json.Marshal(returnBody) + if err != nil { + log.Printf("[ERROR] Failed to marshal retStruct in single execution: %s", err) + } + + resp.Write(returnBytes) + return + } + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) return } @@ -2394,8 +1741,8 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { err = shuffle.SetSchedule(ctx, newSchedule) if err != nil { - log.Printf("Failed setting cloud schedule: %s", err) - resp.WriteHeader(401) + log.Printf("[ERROR] Failed setting cloud schedule: %s", err) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } @@ -2430,17 +1777,22 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - real error message lol if err != nil { - log.Printf("Failed creating schedule: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. Try cron */15 * * * *"}`))) + log.Printf("[ERROR] Failed creating schedule: %s", err) + + resp.WriteHeader(400) + if schedule.Environment == "cloud" { + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. For cloud schedules, try cron */15 * * * *"}`))) + } else { + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid argument. For onprem schedules, try 60 for 60 seconds"}`))) + } return } //workflow.Schedules = append(workflow.Schedules, schedule) err = shuffle.SetWorkflow(ctx, *workflow, workflow.ID) if err != nil { - log.Printf("Failed setting workflow for schedule: %s", err) - resp.WriteHeader(401) + log.Printf("[ERROR] Failed setting workflow for schedule: %s", err) + resp.WriteHeader(400) resp.Write([]byte(`{"success": false}`)) return } @@ -3432,19 +2784,63 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) { return } + ctx := shuffle.GetContext(request) user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { - log.Printf("[WARNING] Api authentication failed in execute SINGLE workflow - CONTINUING ANYWAY: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "You need to sign up to try it out}`)) - return - } + // Look for org_id query as app may be private + // No validation is done here, as it's just running the app + // to find a user + orgId := request.URL.Query().Get("org_id") + if len(orgId) > 0 { + user.ActiveOrg.Id = orgId + } else { + executionId := request.URL.Query().Get("execution_id") + authorization := request.URL.Query().Get("authorization") + if len(executionId) == 0 || len(authorization) == 0 { + log.Printf("[WARNING] Bad execution id/auth in single action validate (1): %#v, %#v. Continuing with the 'public' org id", executionId, authorization) + err := shuffle.ValidateRequestOverload(resp, request) + if err != nil { + log.Printf("[INFO] Request overload for IP %s in single action execution", shuffle.GetRequestIp(request)) + resp.WriteHeader(429) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Too many requests. Please try again in 30 seconds."}`))) + return + } - if user.Role == "org-reader" { - log.Printf("[WARNING] Org-reader doesn't have access to execute single action: %s (%s)", user.Username, user.Id) - resp.WriteHeader(403) - resp.Write([]byte(`{"success": false, "reason": "Read only user"}`)) - return + user.Username = shuffle.GetRequestIp(request) + user.ActiveOrg.Name = shuffle.GetRequestIp(request) + user.ActiveOrg.Id = "public" + } else { + // Find the execution + exec, err := shuffle.GetWorkflowExecution(ctx, executionId) + if err != nil { + log.Printf("[WARNING] Bad execution id in single action validate (2): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Bad execution mapping (1)"}`)) + return + } + + if exec.Authorization != authorization { + log.Printf("[WARNING] Bad execution auth in single action validate (3): %#v, %#v", exec.Authorization, authorization) + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false, "reason": "Bad execution mapping (2)"}`)) + return + } + + //log.Printf("[INFO] Found org_id from execution: %#v. Executionorg: %#v", exec.OrgId, exec.ExecutionOrg) + user.ActiveOrg.Id = exec.OrgId + if len(user.ActiveOrg.Id) == 0 { + user.ActiveOrg.Id = exec.ExecutionOrg + } + + user.Username = fmt.Sprintf("org %s", user.ActiveOrg.Id) + } + } + + if len(user.ActiveOrg.Id) == 0 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "No org_id found to map back to"}`)) + return + } } location := strings.Split(request.URL.String(), "/") @@ -3459,93 +2855,117 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) { fileId = location[4] } + //log.Printf("[AUDIT] User Authentication failed in execute SINGLE action - CONTINUING ANYWAY: %s. Found OrgID: %#v", err, user.ActiveOrg.Id) + log.Printf("[AUDIT] User %s (%s) in org %s (%s) is running SINGLE App run for App ID '%s'", user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id, fileId) + body, err := ioutil.ReadAll(request.Body) if err != nil { - log.Printf("[INFO] Failed workflowrequest POST read: %s", err) + log.Printf("[INFO] Failed single execution POST body read: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return } - ctx := context.Background() - + // Look for the query parameter "validation=true" to find the correct action for the app to test runValidationAction := false query := request.URL.Query() validation, ok := query["validation"] - if ok && validation[0] == "true" { + if ok && len(validation) > 0 && validation[0] == "true" { runValidationAction = true } - workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, fileId, body, runValidationAction) - if err != nil { - log.Printf("[INFO] Failed workflowrequest POST read: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return + shouldRerun := false + rerun, rerunOk := query["rerun"] + if rerunOk && len(rerun) > 0 && rerun[0] == "true" { + shouldRerun = true } + decisionId := "" + decision, decisionOk := query["decision_id"] + if decisionOk && len(decision) > 0 { + decisionId = decision[0] + } + + workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, fileId, body, runValidationAction, decisionId) + debugUrl := fmt.Sprintf("/workflows/%s?execution_id=%s", workflowExecution.Workflow.ID, workflowExecution.ExecutionId) resp.Header().Add("X-Debug-Url", debugUrl) - workflowExecution.Priority = 11 - environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id) - environment := "Shuffle" - if len(environments) >= 1 { - // Find default one - environment = environments[0].Name - - for _, env := range environments { - if env.Default { - environment = env.Name - break - } + if err != nil { + log.Printf("[INFO] Failed workflowrequest POST read in single action (4): %s", err) + returndata := shuffle.ResultChecker{ + Success: false, + Reason: fmt.Sprintf("%s", err), } - } else { - log.Printf("[ERROR] No environments found for org %s. Exiting", user.ActiveOrg.Id) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) + resp.WriteHeader(400) + respBytes, err := json.Marshal(returndata) + if err != nil { + resp.Write([]byte(`{"success": false}`)) + return + } + + resp.Write(respBytes) return } - // Enforcing same env for job + run to be default - // FIXME: Should use environment that is in the source workflow if it exists - for i, _ := range workflowExecution.Workflow.Actions { - workflowExecution.Workflow.Actions[i].Environment = environment - workflowExecution.Workflow.Actions[i].Label = "TMP" + workflowExecution.ProjectId = "" + workflowExecution.Locations = []string{""} + + foundEnv := "" + params := []string{} + for _, action := range workflowExecution.Workflow.Actions { + for _, param := range action.Parameters { + params = append(params, param.Name) + } + + if len(action.Environment) > 0 { + foundEnv = action.Environment + break + } } - shuffle.SetWorkflowExecution(ctx, workflowExecution, false) - - log.Printf("[INFO] Execution (single action): %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID) + go shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions") executionRequest := shuffle.ExecutionRequest{ ExecutionId: workflowExecution.ExecutionId, WorkflowId: workflowExecution.Workflow.ID, Authorization: workflowExecution.Authorization, - Environments: []string{environment}, - Priority: 11, + Environments: []string{foundEnv}, } - executionRequest.Priority = workflowExecution.Priority - err = shuffle.SetWorkflowQueue(ctx, executionRequest, environment) + parsedEnv := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(foundEnv, " ", "-"), "_", "-")) + + log.Printf("[INFO] Adding new single-action job to env queue (4): %s", parsedEnv) + err = shuffle.SetWorkflowQueue(ctx, executionRequest, parsedEnv) if err != nil { - log.Printf("[ERROR] Failed adding execution to db: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) + log.Printf("[WARNING] Failed adding %s to db (single action queue): %s", parsedEnv, err) + } + + 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 } - time.Sleep(2 * time.Second) - log.Printf("[INFO] Starting validation of execution %s", workflowExecution.ExecutionId) + actionId := "" + if len(workflowExecution.Workflow.Actions) == 1 { + actionId = workflowExecution.Workflow.Actions[0].ID + } - returnBody := shuffle.HandleRetValidation(ctx, workflowExecution, 1) + returnBody := shuffle.HandleRetValidation(ctx, workflowExecution, 1, actionId) returnBytes, err := json.Marshal(returnBody) if err != nil { log.Printf("[ERROR] Failed to marshal retStruct in single execution: %s", err) } - // Deleting as this is a single action and doesn't need to be stored - shuffle.DeleteKey(ctx, "workflowexecution", executionRequest.ExecutionId) + // Look for delete=true query, and if it exists, delete the execution + if request.URL.Query().Get("delete") == "true" { + err = shuffle.DeleteKey(ctx, "workflowexecution", workflowExecution.ExecutionId) + if err != nil { + log.Printf("[ERROR] Failed to delete execution: %s", err) + } + } resp.WriteHeader(200) resp.Write([]byte(returnBytes)) diff --git a/docker-compose.yml b/docker-compose.yml index 84b1f321..0ff2c29c 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: frontend: - image: ghcr.io/shuffle/shuffle-frontend:latest + image: ghcr.io/shuffle/shuffle-frontend:nightly container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -14,7 +14,7 @@ services: depends_on: - backend backend: - image: ghcr.io/shuffle/shuffle-backend:latest + image: ghcr.io/shuffle/shuffle-backend:nightly container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -33,7 +33,7 @@ services: - SHUFFLE_FILE_LOCATION=/shuffle-files restart: unless-stopped orborus: - image: ghcr.io/shuffle/shuffle-orborus:latest + image: ghcr.io/shuffle/shuffle-orborus:nightly container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -53,15 +53,15 @@ services: - SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY} - SHUFFLE_PASS_APP_PROXY=${SHUFFLE_PASS_APP_PROXY} - SHUFFLE_STATS_DISABLED=true - - SHUFFLE_WORKER_SCALE=run - SHUFFLE_LOGS_DISABLED=true - - SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest + - SHUFFLE_SWARM_CONFIG=run + - SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:nightly env_file: .env restart: unless-stopped security_opt: - seccomp:unconfined opensearch: - image: opensearchproject/opensearch:2.14.0 + image: opensearchproject/opensearch:3.0.0 hostname: shuffle-opensearch container_name: shuffle-opensearch environment: diff --git a/frontend/frontend-testing.sh b/frontend/frontend-testing.sh new file mode 100644 index 00000000..c25c3618 --- /dev/null +++ b/frontend/frontend-testing.sh @@ -0,0 +1,74 @@ + +SERVER_URL="http://localhost:3000" + +declare -a Routes=($(grep -oP '(?<=path=")[^"]*' src/App.jsx | grep -E '^[a-zA-Z0-9/_:-]+$' | grep -vE '^/$')) + +for i in "${!Routes[@]}"; do + Routes[$i]="${Routes[$i]#/}" +done + +# # Add all tab routes and unique routes here +Routes+=( +'workflows?tab=org_workflows' +'workflows?tab=my_workflows' +'workflows?tab=all_workflows' +'apps/gmail' +'apis/gmail' +'apps?tab=my_apps' +'apps?tab=all_apps' +'search?tab=org_apps' +'search?tab=my_apps' +'search?tab=workflows' +'search?tab=docs' +'search?tab=creators' +'search?tab=discord' +"admin?tab=organization" +"admin?tab=users" +"admin?tab=app_auth" +"admin?tab=datastore" +"admin?tab=files" +"admin?tab=triggers" +"admin?tab=locations" +"admin?tab=tenants" +"admin?admin_tab=org_config" +"admin?admin_tab=sso" +"admin?admin_tab=notifications" +"admin?admin_tab=billingstats" +"admin?admin_tab=branding(beta)" +) + +# Stop frontend container so it test unpushed changes +docker stop shuffle-frontend + +# # Install all frontend dependencies +yarn add selenium-webdriver --dev && yarn install +echo "Starting frontend..." + +BROWSER=none yarn start & +SERVER_PID=$! +echo "Frontend started with PID: $SERVER_PID" + +echo "Waiting for 1 minute to ensure the server is fully up..." +sleep 60 + +echo "Server is up! Starting Selenium tests..." + +echo "Starting frontend tests..." +node selenium-test.js "$SERVER_URL" "${Routes[@]}" + +TEST_EXIT_CODE=$? + +if [[ $TEST_EXIT_CODE -ne 0 ]]; then +echo "Selenium tests failed. Exiting..." +kill $SERVER_PID +exit 1 +fi + +kill $SERVER_PID +echo "Testing complete. See above logs for errors if any." + +# Starting shuffle-frontend container +echo "Starting shuffle-frontend container..." +docker start shuffle-frontend +echo "shuffle-frontend started successfully." +exit 0 diff --git a/frontend/package.json b/frontend/package.json old mode 100755 new mode 100644 index 7426ee23..db88fae3 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "@uiw/codemirror-themes": "^4.21.9", "@uiw/react-codemirror": "^4.21.21", "algoliasearch": "^4.8.3", - "class-transformer": "^0.2.0", + "class-transformer": "^0.5.1", "codemirror": "^6.0.1", "cpx": "^1.5.0", "create-react-app": "^5.0.1", @@ -128,6 +128,7 @@ "babel-preset-es2015": "^6.24.1", "postcss": "^8.4.38", "promise-window": "^1.2.1", + "selenium-webdriver": "^4.29.0", "webpack-cli": "^5.1.4" } } diff --git a/frontend/public/images/ProfessionalServices.svg b/frontend/public/images/ProfessionalServices.svg new file mode 100644 index 00000000..164b6917 --- /dev/null +++ b/frontend/public/images/ProfessionalServices.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/SecurityConsultation.svg b/frontend/public/images/SecurityConsultation.svg new file mode 100644 index 00000000..05614a4d --- /dev/null +++ b/frontend/public/images/SecurityConsultation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/Support.svg b/frontend/public/images/Support.svg new file mode 100644 index 00000000..83a18f00 --- /dev/null +++ b/frontend/public/images/Support.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/Training.svg b/frontend/public/images/Training.svg new file mode 100644 index 00000000..b01a9901 --- /dev/null +++ b/frontend/public/images/Training.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/API.svg b/frontend/public/images/icons/API.svg new file mode 100644 index 00000000..447b94de --- /dev/null +++ b/frontend/public/images/icons/API.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/about_us.svg b/frontend/public/images/icons/about_us.svg new file mode 100644 index 00000000..c77e7e8e --- /dev/null +++ b/frontend/public/images/icons/about_us.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/about_us_hover.svg b/frontend/public/images/icons/about_us_hover.svg new file mode 100644 index 00000000..2c693ca9 --- /dev/null +++ b/frontend/public/images/icons/about_us_hover.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/articles.svg b/frontend/public/images/icons/articles.svg new file mode 100644 index 00000000..763d960f --- /dev/null +++ b/frontend/public/images/icons/articles.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/images/icons/articles_hover.svg b/frontend/public/images/icons/articles_hover.svg new file mode 100644 index 00000000..6b51f294 --- /dev/null +++ b/frontend/public/images/icons/articles_hover.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/images/icons/contact_us.svg b/frontend/public/images/icons/contact_us.svg new file mode 100644 index 00000000..5fbc3568 --- /dev/null +++ b/frontend/public/images/icons/contact_us.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/contact_us_hover.svg b/frontend/public/images/icons/contact_us_hover.svg new file mode 100644 index 00000000..0da02b7c --- /dev/null +++ b/frontend/public/images/icons/contact_us_hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/discord.svg b/frontend/public/images/icons/discord.svg new file mode 100644 index 00000000..b982260b --- /dev/null +++ b/frontend/public/images/icons/discord.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/icons/docs.svg b/frontend/public/images/icons/docs.svg new file mode 100644 index 00000000..a568538d --- /dev/null +++ b/frontend/public/images/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/docs_hover.svg b/frontend/public/images/icons/docs_hover.svg new file mode 100644 index 00000000..a711ba59 --- /dev/null +++ b/frontend/public/images/icons/docs_hover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/faq.svg b/frontend/public/images/icons/faq.svg new file mode 100644 index 00000000..7345040f --- /dev/null +++ b/frontend/public/images/icons/faq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/icons/faq_hover.svg b/frontend/public/images/icons/faq_hover.svg new file mode 100644 index 00000000..4d29215a --- /dev/null +++ b/frontend/public/images/icons/faq_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/icons/github.svg b/frontend/public/images/icons/github.svg new file mode 100644 index 00000000..de6c0200 --- /dev/null +++ b/frontend/public/images/icons/github.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/images/icons/linkedIn.svg b/frontend/public/images/icons/linkedIn.svg new file mode 100644 index 00000000..605e7804 --- /dev/null +++ b/frontend/public/images/icons/linkedIn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/icons/shuffleLogo.svg b/frontend/public/images/icons/shuffleLogo.svg new file mode 100755 index 00000000..1024dd6a --- /dev/null +++ b/frontend/public/images/icons/shuffleLogo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/icons/usecases.svg b/frontend/public/images/icons/usecases.svg new file mode 100644 index 00000000..cc96e086 --- /dev/null +++ b/frontend/public/images/icons/usecases.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/usecases_hover.svg b/frontend/public/images/icons/usecases_hover.svg new file mode 100644 index 00000000..6841b21e --- /dev/null +++ b/frontend/public/images/icons/usecases_hover.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/x.svg b/frontend/public/images/icons/x.svg new file mode 100644 index 00000000..815e4e30 --- /dev/null +++ b/frontend/public/images/icons/x.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/logos/singul.svg b/frontend/public/images/logos/singul.svg new file mode 100644 index 00000000..5f27d7a3 --- /dev/null +++ b/frontend/public/images/logos/singul.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/singul_green.png b/frontend/public/images/singul_green.png new file mode 100644 index 00000000..796ad6ef Binary files /dev/null and b/frontend/public/images/singul_green.png differ diff --git a/frontend/selenium-test.js b/frontend/selenium-test.js new file mode 100644 index 00000000..42b6b801 --- /dev/null +++ b/frontend/selenium-test.js @@ -0,0 +1,253 @@ +const { Builder, By, until } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); +const fs = require('fs'); +const path = require('path'); + +console.log('Starting Selenium script for testing pages...'); + +(async () => { + const userDataDir = path.join(__dirname, 'chrome-user-data', `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`); + fs.mkdirSync(userDataDir, { recursive: true }); + + let options = new chrome.Options(); + options.addArguments('--no-sandbox'); + options.addArguments('--headless'); + + let driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(options) + .build(); + + const SuccessfullyLoadedPath = []; + const FailedToLoadPath = []; + + try { + await driver.manage().window().setRect({ width: 1600, height: 1200 }); + await driver.manage().window().maximize() + await driver.manage().setTimeouts({ implicit: 15000 }); + + const frontendURL = process.argv[2]; + const routes = process.argv.slice(3); + + console.log('Frontend URL:', frontendURL); + const isCloud = frontendURL === 'http://localhost:3002' || frontendURL === 'https://sandbox.shuffler.io' || frontendURL === 'https://shuffler.io'; + + if (isCloud) { + // Login Credentials + const LOGIN_URL = `${frontendURL}/login`; + // put your login credentials here + const USERNAME = ''; + const PASSWORD = ''; + + console.log('Logging in...'); + await driver.get(LOGIN_URL); + + try { + await driver.wait(until.elementLocated(By.css('#emailfield')), 10000); + await driver.findElement(By.css('#emailfield')).sendKeys(USERNAME); + + await driver.wait(until.elementLocated(By.css('#outlined-password-input')), 10000); + await driver.findElement(By.css('#outlined-password-input')).sendKeys(PASSWORD); + + await driver.wait(until.elementLocated(By.css('#loginButton')), 10000); + await driver.findElement(By.css('#loginButton')).click(); + + // Ensure login success by checking URL change + await driver.wait(async () => { + const url = await driver.getCurrentUrl(); + return url.includes('welcome'); + }, 20000); + + console.log('Successfully logged in!'); + } catch (error) { + console.error('Login failed:', error.message); + FailedToLoadPath.push(LOGIN_URL); + await driver.quit(); + process.exit(1); + } + }else { + // Steps for onprem testing + // 1. Login + // 2. Create new workflow + + // Write your own login credentials here if you are testing onprem locally + const USERNAME = 'demo@demo.io'; + const PASSWORD = 'supercoolpassword'; + const WORKFLOW_URL = `${frontendURL}/workflows`; + + try { + // Login + console.log('Logging in...'); + await driver.get(`${frontendURL}/login`); + + await driver.wait(until.elementLocated(By.css('#emailfield')), 10000); + await driver.findElement(By.css('#emailfield')).sendKeys(USERNAME); + + await driver.wait(until.elementLocated(By.css('#outlined-password-input')), 10000); + await driver.findElement(By.css('#outlined-password-input')).sendKeys(PASSWORD); + + const loginButton = await driver.findElement(By.css('#loginButton')); + await driver.executeScript("arguments[0].scrollIntoView(true);", loginButton); + await loginButton.click(); + + + + // Ensure signup success by checking URL change + await driver.wait(async () => { + const url = await driver.getCurrentUrl(); + return url.includes('welcome') || url.includes('workflows'); + }, 20000); + + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:') + }); + + console.error(`Page (${frontendURL}/login) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(`${frontendURL}/login`); + } + } + + console.log('Successfully logged in!'); + + } catch (error) { + console.error('Failed to login:', error.message); + FailedToLoadPath.push(WORKFLOW_URL); + await driver.quit(); + process.exit(1); + } + + try { + // Create new workflow + console.log('Creating new workflow...'); + await driver.get(WORKFLOW_URL); + + await driver.sleep(1000); + + await driver.wait(until.elementLocated(By.css('#create_workflow_button')), 10000); + const create_workflow_button = await driver.findElement(By.css('#create_workflow_button')); + + await driver.sleep(1000); + await create_workflow_button.click(); + + await driver.sleep(1500); + + await driver.wait(until.elementLocated(By.css('#Enter-Workflow-Name')), 10000); + const enter_workflow_name_field = await driver.findElement(By.css('#Enter-Workflow-Name')); + + await driver.sleep(1000); + await enter_workflow_name_field.sendKeys("Test Workflow"); + + // Wait before saving + await driver.sleep(1500); + + await driver.wait(until.elementLocated(By.css('#save_workflow_button')), 10000); + const save_workflow_button = await driver.findElement(By.css('#save_workflow_button')); + + await driver.sleep(1000); // Delay before clicking + await save_workflow_button.click(); + + // Wait for the parent component to appear + await driver.sleep(2000); + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:'); + }); + + console.error(`Page (${WORKFLOW_URL}) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(`${WORKFLOW_URL}`); + } + } + + } catch (error) { + console.error('Failed to create new workflow:', error.message); + FailedToLoadPath.push(WORKFLOW_URL); + await driver.quit(); + process.exit(1); + } + } + + // Get routes from command line arguments + if (routes.length === 0) { + console.error('No routes found to test.'); + await driver.quit(); + process.exit(1); + } + console.log(`Found ${routes.length} routes to test.`); + + for (const route of routes) { + const url = `${frontendURL}/${route}`; + console.log(`Testing route: ${url}`); + + try { + await driver.get(url); + await driver.sleep(3000); + + // Wait for document readiness + await driver.wait(async () => { + return await driver.executeScript('return document.readyState') === 'complete'; + }, 10000); + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:') + }); + + console.error(`Page (${url}) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(url); + } + continue; + } + + console.log(`Successfully loaded: ${url}`); + SuccessfullyLoadedPath.push(url); + } catch (error) { + console.error(`Failed to load ${url}:`, error.message); + } + } + } finally { + console.log("Total pages tested: ", SuccessfullyLoadedPath.length + FailedToLoadPath.length); + console.log("Successfully loaded pages: ", SuccessfullyLoadedPath.length); + if (FailedToLoadPath.length > 0) { + console.log("Failed to load pages: ", FailedToLoadPath.length); + console.log("Failed to load pages paths: ", FailedToLoadPath); + process.exit(1); + }else { + console.log("No pages failed to load. Congrats!"); + } + await driver.quit(); + fs.rmSync(userDataDir, { recursive: true, force: true }); + } +})(); \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8c0e8870..13289352 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { Link, Route, Routes, BrowserRouter, useNavigate } from "react-router-dom"; import { CookiesProvider } from "react-cookie"; @@ -12,12 +12,16 @@ import Header from "./components/NewHeader.jsx"; import HealthPage from "./components/HealthPage.jsx"; //import Header from "./components/Header.jsx"; -import theme from "./theme.jsx"; +import theme, { getTheme } from "./theme.jsx"; import Apps from "./views/Apps.jsx"; import Apps2 from "./views/Apps2.jsx"; import AppCreator from "./views/AppCreator.jsx"; import DetectionDashBoard from "./views/DetectionDashboard.jsx"; +// LLM related tests +import ChatBot from "./components/ChatBot.jsx"; +import AgentUI from "./views/AgentUI.jsx"; + import Welcome from "./views/Welcome.jsx"; import Dashboard from "./views/Dashboard.jsx"; import DashboardView from "./views/DashboardViews.jsx"; @@ -25,6 +29,7 @@ import AdminSetup from "./views/AdminSetup.jsx"; import Admin from "./views/Admin.jsx"; import Docs from "./views/Docs.jsx"; import Usecases2 from "./views/Usecases2.jsx"; +import DashboardViews from "./views/DashboardViews.jsx"; //import Introduction from "./views/Introduction"; import SetAuthentication from "./views/SetAuthentication.jsx"; import SetAuthenticationSSO from "./views/SetAuthenticationSSO.jsx"; @@ -33,9 +38,10 @@ import RunWorkflow from "./views/RunWorkflow.jsx"; import Admin2 from "./views/Admin2.jsx"; import LoginPage from "./views/LoginPage.jsx"; +import LoginPageOld from "./views/LoginPageOld.jsx"; + import SettingsPage from "./views/SettingsPage.jsx"; import KeepAlive from "./views/KeepAlive.jsx"; - import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from '@mui/material/CssBaseline'; @@ -57,7 +63,8 @@ import 'react-toastify/dist/ReactToastify.css'; import Drift from "react-driftjs"; -import { AppContext } from './context/ContextApi.jsx'; +import { Context } from './context/ContextApi.jsx'; +import Navbar from "./components/Navbar.jsx"; import Workflows2 from "./views/Workflows2.jsx"; import AppExplorer from "./views/AppExplorer.jsx"; @@ -85,7 +92,10 @@ const App = (message, props) => { const [dataset, setDataset] = useState(false) const [isLoaded, setIsLoaded] = useState(false) const [curpath, setCurpath] = useState(typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname) - + const { themeMode, handleThemeChange, setBrandColor, brandColor,setThemeMode} = useContext(Context); + const currentTheme = getTheme(themeMode, brandColor); + const mainColor = currentTheme?.palette?.backgroundColor + const [isPreviousThemeLight, setIsPreviousThemeLight] = useState(false) useEffect(() => { if (dataset === false) { @@ -95,6 +105,29 @@ const App = (message, props) => { } }, []); + useEffect(() => { + const isDarkPath = curpath === "/" || curpath === "/pricing" || curpath === "/partners" || curpath === "/faq" || curpath === "/professional-services" || curpath === "/contact" || curpath === "/training"; + if (curpath && isDarkPath) { + if (themeMode === "light") { + setIsPreviousThemeLight(true) + } + handleThemeChange("dark") + }else if ( !isDarkPath && isPreviousThemeLight && userdata?.active_org?.branding?.theme === "light") { + handleThemeChange("light") + setIsPreviousThemeLight(false) + } + + if (isDarkPath && userdata && userdata?.active_org?.branding?.brand_color !== "#ff8544") { + setBrandColor("#ff8544") + }else if(!isDarkPath && userdata && userdata?.active_org?.branding?.brand_color !== "#ff8544" && userdata?.active_org?.branding?.brand_color?.length > 0) { + const brandColor = localStorage.getItem("brandColor") + if (brandColor !== null && brandColor !== undefined && brandColor.length > 0) { + setBrandColor(brandColor) + } + } + + }, [themeMode, curpath, userdata]) + if ( isLoaded && !isLoggedIn && @@ -159,7 +192,16 @@ const App = (message, props) => { { path: "/" } ); } - } + if (responseJson?.theme?.length > 0) { + handleThemeChange(responseJson.theme) + }else{ + handleThemeChange("dark") + } + }else { + handleThemeChange("dark") + setThemeMode("dark") + localStorage.removeItem("theme"); + } // Handling Ethereum update @@ -182,10 +224,11 @@ const App = (message, props) => { const includedData =
{ { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
-
*/} + {
*/} + { { /> } /> + + + } + /> + + + } + /> + + + } + /> + { /> } /> - } /> + } /> { } /> ) : null} + { /> } /> + { checkLogin={checkLogin} userdata={userdata} globalUrl={globalUrl} - surfaceColor={theme.palette.surfaceColor} - inputColor={theme.palette.inputColor} + surfaceColor={currentTheme.palette.surfaceColor} + inputColor={currentTheme.palette.inputColor} {...props} /> } @@ -476,8 +594,8 @@ const App = (message, props) => { /> } /> - } /> - } /> + } /> + } /> { /> } /> - } /> - } /> - } /> + } /> + } /> + } /> - } /> - } /> - } /> + } /> + } /> + } /> + + } + /> + + } + /> { exact path="/docs" element={ - //navigate(`/docs/about`) { /> } /> - } /> + } /> { /> } /> - - } - /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> { /> } /> + + + } + /> + {
return ( - - + @@ -722,11 +954,10 @@ const App = (message, props) => { pauseOnFocusLoss draggable pauseOnHover - theme="dark" + theme={themeMode} /> - ); }; diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index c6f70357..2a480c95 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useContext, memo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import OrganizationTab from '../components/OrganizationTab.jsx'; +import PartnerTab from '../components/PartnerTab.jsx'; import UserManagmentTab from '../components/UserManagmentTab.jsx'; import CacheView from "../components/CacheView.jsx"; import Files from "../components/Files.jsx"; @@ -19,24 +20,124 @@ import { FmdGoodOutlined as FmdGoodOutlinedIcon, GroupOutlined as GroupOutlinedIcon } from '@mui/icons-material'; -import theme from '../theme.jsx'; -import { Button, Tooltip } from '@mui/material'; +import theme, { getTheme } from '../theme.jsx'; +import { Button, Skeleton, Tooltip } from '@mui/material'; import { Index } from 'react-instantsearch-dom'; import { Context } from '../context/ContextApi.jsx'; +import { toast } from 'react-toastify'; + +const PartnerIcon = ({ strokeColor, fillColor = 'transparent', width = 22, height = 22 }) => ( + + + +); const AdminNavBar = (props) => { const location = useLocation(); - const { globalUrl, userdata, isCloud, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props; + const { globalUrl, userdata, isCloud,isOrgLoaded, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props; const [selectedItem, setSelectedItem] = useState("Organization"); const [isSelectedFiles, setIsSelectedFiles] = useState(true); const [isSelectedDataStore, setIsSelectedDataStore] = useState(true); + const [isIntegrationPartner, setIsIntegrationPartner] = useState(false); + const [isChildOrg, setIsChildOrg] = useState(false); + const [isGlobalUser, setIsGlobalUser] = useState(false); + const [visibleItems, setVisibleItems] = useState([]); + const [isUserDataLoaded, setIsUserDataLoaded] = useState(false); + + + useEffect(() => { + if (userdata && userdata?.active_org?.id?.length > 0) { + setIsUserDataLoaded(true); + } + }, [userdata]); + + + + const { themeMode, brandColor } = React.useContext(Context); + const theme = getTheme(themeMode, brandColor); + + const HandlePartnerChange = () => { + if (userdata?.id?.length > 0) { + const isIntegrationPartner = userdata?.org_status?.includes("integration_partner") || false; + setIsIntegrationPartner(isIntegrationPartner); + const isChildOrg = userdata?.org_status?.includes("sub_org") || false; + setIsChildOrg(isChildOrg); + const isGlobalUser = userdata?.active_org?.branding?.global_user || false; + setIsGlobalUser(isGlobalUser); + } else { + setIsIntegrationPartner(false); + setIsChildOrg(false); + setIsGlobalUser(false); + } + } + + useEffect(() => { + if (userdata && userdata?.id?.length > 0) { + HandlePartnerChange(); + } + }, [userdata]); + + const HandleVisibleTabs = () => { + if (userdata?.id?.length > 0) { + if (userdata?.active_org?.role === "admin" || userdata?.support) { + setVisibleItems(items); + }else { + const filteredItems = items.filter(item => item.text !== "Users" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations"); + setVisibleItems(filteredItems); + } + } + } + + useEffect(() => { + if (isIntegrationPartner && isChildOrg && !isGlobalUser) { + // Filter out Users and Tenants tabs + if (userdata?.active_org?.role === "admin" || userdata?.support) { + const filteredItems = items.filter(item => + item.text !== "Users" && item.text !== "Tenants" + ); + setVisibleItems(filteredItems); + }else { + const filteredItems = items.filter(item => + item.text !== "Users" && item.text !== "Tenants" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations" + ); + setVisibleItems(filteredItems); + } + } else { + HandleVisibleTabs(); + } + }, [isIntegrationPartner, isChildOrg, isGlobalUser, selectedOrganization, userdata]); 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'); - + const partnerTab = queryParams.get('partner_tab'); + + // if(!isCloud){ + // if(tabName === "partner" || partnerTab !== null) { + // setSelectedItem("Organization"); + // navigate(`?tab=organization`, { replace: true }); + // } + // } + if (partnerTab) { + setSelectedItem("Partner"); + } if (tabName === "environments") { setSelectedItem("Locations"); } else if (tabName === "suborgs") { @@ -45,14 +146,15 @@ const AdminNavBar = (props) => { setSelectedItem("Datastore"); }else if (tabName) { setSelectedItem(tabName.charAt(0).toUpperCase() + tabName.slice(1)); - } else { - setSelectedItem("Organization"); + }else if (tabName === "partner") { + setSelectedItem("Partner"); } + }, [location.search]); - const items = [ - { iconSrc: , alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } }, + { iconSrc: , alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { isIntegrationPartner, isChildOrg, isGlobalUser,globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } }, + { iconSrc: undefined, alt: "Partner Icon", text: "Partner", component: PartnerTab, props: { globalUrl,removeCookie, isLoaded, handleGetOrg, userdata, isCloud, serverside, checkLogin, setSelectedOrganization, selectedOrganization } }, { iconSrc: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } }, { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} }, @@ -60,7 +162,7 @@ const AdminNavBar = (props) => { { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } }, { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, { iconSrc: , alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } } - ]; + ].filter(Boolean); const setConfig = (newValue) => { setSelectedItem(newValue); @@ -73,19 +175,80 @@ const AdminNavBar = (props) => { } }; + useEffect(() => { + if (isIntegrationPartner && isChildOrg && !isGlobalUser && isOrgLoaded && isUserDataLoaded) { + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams?.get('admin_tab')?.toLowerCase(); + if (tabName === "sso" || tabName === "branding") { + toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); + setTimeout(() => { + setSelectedItem("Organization"); + navigate(`?admin_tab=org_config`, { replace: true }); + window.location.reload(); + } + , 3000); + } + + const params = new URLSearchParams(location.search); + const tab = params?.get('tab')?.toLowerCase(); + if (tab === "users" || tab === "tenants") { + toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); + setTimeout(() => { + setSelectedItem("Organization"); + navigate(`?admin_tab=org_config`, { replace: true }); + window.location.reload(); + } + , 3000); + } + } else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) { + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams?.get('admin_tab')?.toLowerCase(); + if (tabName === "sso") { + toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); + setTimeout(() => { + setSelectedItem("Organization"); + navigate(`?admin_tab=org_config`, { replace: true }); + window.location.reload(); + } + , 3000); + } + + const params = new URLSearchParams(location.search); + const tab = params?.get('tab')?.toLowerCase(); + if (tab === "users" || tab === "locations" || tab === "environments" || tab === "files" || tab === "datastore" || tab === "triggers") { + toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); + setTimeout(() => { + setSelectedItem("Organization"); + navigate(`?admin_tab=org_config`, { replace: true }); + window.location.reload(); + } + , 3000); + } + } + }, [isIntegrationPartner, isChildOrg, isGlobalUser, location.search, userdata, isOrgLoaded, isUserDataLoaded]); + + const renderComponent = () => { - const selectedItemData = items.find(item => item.text === selectedItem); + const selectedItemData = visibleItems.find(item => item.text === selectedItem); if (!selectedItemData) { setSelectedItem("Organization"); // If no tab is specified, default to "Organization" tab - return ; + return ; }; const ComponentToRender = selectedItemData.component; const componentProps = selectedItemData.props; - return ; - }; + const updatedProps = { + ...componentProps, + notifications: notifications, + setNotifications: setNotifications, + userdata: userdata, + selectedOrganization: selectedOrganization + }; + + return ; + }; const defaultImage = "/images/logos/orange_logo.svg" const imageData = @@ -94,15 +257,16 @@ const AdminNavBar = (props) => { : selectedOrganization?.image; return ( + !isOrgLoaded && !isUserDataLoaded ? :
-
- {renderComponent()} + + + {renderComponent()} +
); }; export default AdminNavBar; +const Loader = () => { + const dummyItems = Array.from({ length: 6 }); + const dummyNavItems = Array.from({ length: 6 }); + const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding']; + const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context); + const theme = getTheme(themeMode); + return ( +
+ +
+
+ + +
+ + {/* Divider */} + + + {/* Nav Items */} +
+ {dummyNavItems.map((_, index) => ( +
+ + +
+ ))} +
+
+
+
+ {dummyTabItems.map((_, index) => ( +
+ +
+ ))} +
+
+
+ + +
+ {dummyItems.map((_, index) => ( +
+ +
+ ))} +
+
+
+
+
+ ); +}; + + const PaddingWrapper2 = memo(({ children }) => { return ( -
+
{children}
) diff --git a/frontend/src/components/AnalyticsTab.jsx b/frontend/src/components/AnalyticsTab.jsx index 8938262e..c97560ed 100644 --- a/frontend/src/components/AnalyticsTab.jsx +++ b/frontend/src/components/AnalyticsTab.jsx @@ -1,275 +1,278 @@ -import React, { useEffect, useState } from 'react'; -import Switch from '@mui/material/Switch'; -import { Typography, Button } from '@mui/material'; -import { useNavigate, Link, useParams } from "react-router-dom"; -import { Bar } from 'react-chartjs-2'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import NewReleasesIcon from '@mui/icons-material/NewReleases'; -import MailOutlineIcon from '@mui/icons-material/MailOutline'; +// import React, { useEffect, useState } from 'react'; +// import Switch from '@mui/material/Switch'; +// import { Typography, Button } from '@mui/material'; +// import { useNavigate, Link, useParams } from "react-router-dom"; +// import { Bar } from 'react-chartjs-2'; +// import Grid from '@mui/material/Grid'; +// import SearchIcon from '@mui/icons-material/Search'; +// import NewReleasesIcon from '@mui/icons-material/NewReleases'; +// import MailOutlineIcon from '@mui/icons-material/MailOutline'; const AnalyticsTab = (props) => { const { userdata, globalUrl, serverside } = props; - const [checked, setChecked] = useState(false); - const [selectedOption, setSelectedOption] = useState('all'); - const [expand, setExpand] = useState(false) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + + console.log("if you need this, work without react-rechartjs-2") + + // const [checked, setChecked] = useState(false); + // const [selectedOption, setSelectedOption] = useState('all'); + // const [expand, setExpand] = useState(false) + // const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); - const handleOptionChange = (option) => { - setSelectedOption(option); - // You can perform actions based on the selected option, such as filtering data - }; + // const handleOptionChange = (option) => { + // setSelectedOption(option); + // // You can perform actions based on the selected option, such as filtering data + // }; - const handleChange = (event) => { - setChecked(event.target.checked); - }; + // const handleChange = (event) => { + // setChecked(event.target.checked); + // }; - const allData = { - labels: ['Email analysis', 'Email Management', 'EDR to Ticket', 'Ticket Analysis'], - datasets: [ - { - label: 'Revisions', - data: [12, 19, 3, 5], // Data for revisions - backgroundColor: '#FF8444', - borderWidth: 1, - // borderRadius: 60, - barPercentage: 0.7, - categoryPercentage: 0.5, - }, - { - label: 'Runs', - data: [8, 15, 5, 8], // Data for runs - backgroundColor: '#9747FF', - borderWidth: 1, - // borderRadius: 60, - barPercentage: 0.7, - categoryPercentage: 0.5 - } - ] - }; + // const allData = { + // labels: ['Email analysis', 'Email Management', 'EDR to Ticket', 'Ticket Analysis'], + // datasets: [ + // { + // label: 'Revisions', + // data: [12, 19, 3, 5], // Data for revisions + // backgroundColor: '#FF8444', + // borderWidth: 1, + // // borderRadius: 60, + // barPercentage: 0.7, + // categoryPercentage: 0.5, + // }, + // { + // label: 'Runs', + // data: [8, 15, 5, 8], // Data for runs + // backgroundColor: '#9747FF', + // borderWidth: 1, + // // borderRadius: 60, + // barPercentage: 0.7, + // categoryPercentage: 0.5 + // } + // ] + // }; - let data; - if (selectedOption === 'all') { - data = allData; - } else if (selectedOption === 'revisions') { - data = { - labels: allData.labels, - datasets: [allData.datasets[0]] // Show only revisions data - }; - } else if (selectedOption === 'run') { - data = { - labels: allData.labels, - datasets: [allData.datasets[1]] // Show only runs data - }; - } + // let data; + // if (selectedOption === 'all') { + // data = allData; + // } else if (selectedOption === 'revisions') { + // data = { + // labels: allData.labels, + // datasets: [allData.datasets[0]] // Show only revisions data + // }; + // } else if (selectedOption === 'run') { + // data = { + // labels: allData.labels, + // datasets: [allData.datasets[1]] // Show only runs data + // }; + // } - // Options for the chart - const options = { - scales: { - yAxes: [ - { - ticks: { - beginAtZero: true - } - } - ] - }, - }; - return ( -
-
-
Timeline
-
-
-
-
-
-
Apps
- Category -
- { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand -
- {expand ? null : -
-
- Onboarding - - -
- - {checked ? -
: - null - } -
-
- -
- - {checked ? -
: - null - } -
-
- -
- - {checked ? -
: - null - } -
-
-
-
-
-
- Other - - -
- - {checked ? -
: - null - } -
-
- -
- - {checked ? -
: - null - } -
-
- -
- - {checked ? -
: - null - } -
-
-
-
-
} -
-
-
Workflows
-
- - - -
- -
-
-
-
Insights
-
-
-
-
Sessions Overview
-
-
-
- 2.8 Hours -
-
- Avg. Activity per session -
-
-
-
- /usercases/edr to ticket -
-
- Last visited page -
-
-
-
- /workflow/email management -
-
- Most visited page -
-
-
-
-
- ); + // // Options for the chart + // const options = { + // scales: { + // yAxes: [ + // { + // ticks: { + // beginAtZero: true + // } + // } + // ] + // }, + // }; + // return ( + //
+ //
+ //
Timeline
+ //
+ //
+ //
+ //
+ //
+ //
Apps
+ // Category + //
+ // { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand + //
+ // {expand ? null : + //
+ //
+ // Onboarding + // + // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ //
+ //
+ //
+ //
+ // Other + // + // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ // + //
+ // + // {checked ? + //
: + // null + // } + //
+ //
+ //
+ //
+ //
} + //
+ //
+ //
Workflows
+ //
+ // + // + // + //
+ // + //
+ //
+ //
+ //
Insights
+ //
+ //
+ //
+ //
Sessions Overview
+ //
+ //
+ //
+ // 2.8 Hours + //
+ //
+ // Avg. Activity per session + //
+ //
+ //
+ //
+ // /usercases/edr to ticket + //
+ //
+ // Last visited page + //
+ //
+ //
+ //
+ // /workflow/email management + //
+ //
+ // Most visited page + //
+ //
+ //
+ //
+ //
+ // ); }; export default AnalyticsTab; diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index e6080301..d1321220 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -29,7 +29,7 @@ import { Tooltip, } from "@mui/material"; import throttle from "lodash/throttle"; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import { validateJson, collapseField, } from "../views/Workflows.jsx"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -37,6 +37,8 @@ import { Context } from "../context/ContextApi.jsx"; function CustomTabPanel(props) { const { children, value, index, ...other } = props; + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode) return (
{value === index && ( - + {children} )} @@ -100,6 +102,8 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se const [selectedActionIndex, setSelectedActionIndex] = useState(0); const [ExampleBody, setExampleBody] = useState({}); const [filteredActions, setFilteredActions] = useState([]); + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode) const [firstSendDone, setFirstSendDone] = useState(false) @@ -1391,6 +1395,8 @@ const ActionsList = memo(({ const [searchQuery, setSearchQuery] = useState(""); const [visibleActions, setVisibleActions] = useState([]); + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode); useEffect(() => { @@ -1457,13 +1463,13 @@ const ActionsList = memo(({
0 ? openapi?.info["x-logo"] : theme?.palette?.defaultImage} width={48} height={48} alt="app logo" style={{ marginLeft: 20, borderRadius: 8 }} /> - {info.title} @@ -1471,7 +1477,7 @@ const ActionsList = memo(({ ) : ( - Api Explorer @@ -1504,7 +1510,7 @@ const ActionsList = memo(({ style={{ marginLeft: 20, marginTop: 15, - backgroundColor: "#1a1a1a", + backgroundColor: theme.palette.backgroundColor, overflowY: "auto", height: (isLoaded && isLoggedIn) ? "calc(100vh - 190px)" : "calc(100vh - 260px)", paddingRight: 5, @@ -1522,7 +1528,7 @@ const ActionsList = memo(({ textTransform: "none", backgroundColor: selectedActionIndex === actionIndex - ? "#3f3f3f" + ? theme.palette.hoverColor : "transparent", border: "none", justifyContent: "flex-start", @@ -1535,7 +1541,7 @@ const ActionsList = memo(({ textWrap: "nowrap", textOverflow: "ellipsis", "&:hover": { - backgroundColor: "#2f2f2f", + backgroundColor: theme.palette.hoverColor, }, }} onClick={() => handleActionClick(actionIndex, action)} @@ -1554,7 +1560,7 @@ const ActionsList = memo(({ )) ) : ( -
+
No actions found
)} @@ -1602,6 +1608,8 @@ const Action = memo(( const [disableExecuteButton, setDisableExecuteButton] = useState(false); const [showResponseLoader, setShowResponseLoader] = useState(false); const [appAuthentication, setAppAuthentication] = useState([]) + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode); const parseHeaders = (headersString) => { if (headersString?.length > 0) { const headersArray = headersString.split("\n"); @@ -2082,7 +2090,7 @@ const Action = memo(( fontWeight: 700, marginLeft: 40, marginBottom: 5, - color: "rgba(241, 241, 241, 1)", + color: theme.palette.textColor }} > {actionname} @@ -2095,7 +2103,7 @@ const Action = memo(( borderRadius: 6, marginLeft: "40px", marginTop: 2, - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, height: 51, alignItems: 'center', }} @@ -2109,7 +2117,7 @@ const Action = memo(( backgroundColor: "transparent", "& .MuiSelect-select": { color: RequestMethods.find((method) => method.value === selectedMethod) - ?.color || "#212121", + ?.color || theme.palette.textFieldStyle.backgroundColor, }, }} MenuProps={{ @@ -2117,14 +2125,14 @@ const Action = memo(( sx: { padding: 0, margin: 0, - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, }, MenuListProps: { sx: { padding: 0, margin: 0, - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, }, }} @@ -2135,20 +2143,20 @@ const Action = memo(( value={method.value} sx={{ color: method.color, - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, border: "none", marginBottom: 0.25, "&:hover": { backgroundColor: method.color, - color: "#f9fcf5", + color: theme.palette.textFieldStyle.color, }, "&.Mui-selected": { backgroundColor: method.color, - color: "#f9fcf5", + color: theme.palette.textFieldStyle.color, border: "none", "&:hover": { backgroundColor: method.color, - color: "#f9fcf5", + color: theme.palette.textFieldStyle.color, }, }, "&.Mui-focusVisible": { @@ -2167,9 +2175,8 @@ const Action = memo(( inputProps={{ style: { margin: "auto", - backgroundColor: "transparent", border: "none", - color: "rgba(241, 241, 241, 1)", + color: theme.palette.textFieldStyle.color, display: 'flex', height: '100%', alignItems: 'center', @@ -2331,7 +2338,7 @@ const Action = memo(( style={{ display: "flex", justifyContent: "center", - background: "rgba(26, 26, 26, 1)", + background: theme.palette.backgroundColor, }} > ), style: { - backgroundColor: "rgba(33, 33, 33, 1)", + backgroundColor: theme.palette.platformColor, padding: "4px 8px", }, }} @@ -2600,7 +2607,7 @@ const Action = memo(( - + {action.name.replaceAll("_", " ")} -

+

{action.description ? action.description : ""} @@ -2813,6 +2820,8 @@ const ActionResponse = memo(({ apiResponse, ExampleBody, isLoggedIn, isLoaded }) const [responseTabIndex, setResponseTabIndex] = useState(0) const [oldResponse, setOldResponse] = useState(apiResponse) const [highlight, setHighlight] = useState(false) + const {themeMode } = useContext(Context) + const theme = getTheme(themeMode) const MIN_HEIGHT = 50 @@ -2934,7 +2943,7 @@ const ActionResponse = memo(({ apiResponse, ExampleBody, isLoggedIn, isLoaded }) style={{ width: '100%', height: height, - backgroundColor: '#1a1a1a', + backgroundColor: theme.palette.backgroundColor, display: 'flex', flexDirection: 'column', borderTop: '1px solid rgba(255,255,255,0.2)', @@ -2988,7 +2997,7 @@ const ActionResponse = memo(({ apiResponse, ExampleBody, isLoggedIn, isLoaded }) { + + const {themeMode } = useContext(Context) + const theme = getTheme(themeMode) + const handleReactJsonClipboard = (copy) => { const elementName = "copy_element_shuffle"; let copyText = document.getElementById(elementName); @@ -3036,7 +3049,7 @@ const ResponseTabWrapper = memo(({ apiResponse }) => { { return collapseField(jsonField) }} @@ -3049,7 +3062,8 @@ const ResponseTabWrapper = memo(({ apiResponse }) => { )}) const PaddingWrapper = memo(({ isLoggedIn, isLoaded, children }) => { - const { leftSideBarOpenByClick, windowWidth } = useContext(Context); + const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context); + const theme = getTheme(themeMode) return (

{ ? windowWidth >= 1920 ? "calc(100% - 630px)" : "calc(100% - 570px)" : windowWidth >= 1920 ? "calc(100vw - 460px)": "calc(100% - 410px)" : windowWidth >= 1920 ? "calc(100% - 370px)" : "calc(100% - 320px)", - backgroundColor: "#1a1a1a", + backgroundColor: theme.palette.backgroundColor, position: "fixed", bottom: 0, right: 0, diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index fb2af4ff..8c65366e 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -13,7 +13,7 @@ import { } from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import Markdown from "react-markdown"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; import { isMobile } from "react-device-detect" @@ -66,7 +66,7 @@ import { Context } from '../context/ContextApi.jsx'; const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ) const AppAuthTab = memo((props) => { @@ -83,16 +83,17 @@ const AppAuthTab = memo((props) => { const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); const [searchQuery, setSearchQuery] = React.useState(""); const [showAppModal, setShowAppModal] = useState(false) + const [selectedAuthId, setSelectedAuthId] = useState(""); + const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) - const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) - const changeDistribution = (data) => { - //changeDistributed(data, !isDistributed) - editAuthenticationConfig(data.id, "suborg_distribute") - } + const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) + const { themeMode, supportEmail, brandColor } = useContext(Context) + const theme = getTheme(themeMode, brandColor) useEffect(() => { getAppAuthentication(); @@ -212,11 +213,39 @@ const AppAuthTab = memo((props) => { }); }; - const editAuthenticationConfig = (id, parentAction) => { + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + + const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => { const data = { id: id, action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", - } + selected_suborgs: selectedSuborgs !== undefined && selectedSuborgs !== null ? selectedSuborgs : [], + } const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; @@ -238,6 +267,7 @@ const AppAuthTab = memo((props) => { } else { toast("Successfully updated auth!"); setSelectedUserModalOpen(false); + setShowDistributionPopup(false); setTimeout(() => { getAppAuthentication(); }, 1000); @@ -249,6 +279,106 @@ const AppAuthTab = memo((props) => { }); }; + + const changeDistribution = (id, selectedSubOrg) => { + + editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) + } + + + const cacheDistributionModal = showDistributionPopup ? ( + {setShowDistributionPopup(false);setSelectedAuthId("")}} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute Datastore key + + + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; + const editAuthenticationModal = selectedAuthenticationModalOpen ? ( { }} > - + Edit authentication for {selectedAuthentication.app.name.replaceAll("_", " ")} ( {selectedAuthentication.label}) @@ -297,7 +427,7 @@ const AppAuthTab = memo((props) => { InputProps={{ style: { height: 50, - color: "white", + color: theme.palette.textColor, }, }} color="primary" @@ -349,7 +479,7 @@ const AppAuthTab = memo((props) => { InputProps={{ style: { height: 50, - color: "white", + color: theme.palette.textColor, }, }} color="primary" @@ -568,7 +698,7 @@ const AppAuthTab = memo((props) => { }) .then((responseJson) => { if (responseJson.success === false) { - toast("Failed to create. Please try again, or contact support@shuffler.io") + toast(`Failed to create. Please try again, or contact ${supportEmail}`) } else { // Close the modal setAppAuthenticationGroupModalOpen(false) @@ -660,7 +790,7 @@ const AppAuthTab = memo((props) => { }} > - App Authentication Groups + App Authentication Groups @@ -677,7 +807,7 @@ const AppAuthTab = memo((props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textColor, fontSize: "1em", }, }} @@ -842,7 +972,7 @@ const AppAuthTab = memo((props) => { hitsPerPage={5} searchQuery={searchQuery} globalUrl={globalUrl} - isCloud={isCloud} + isCloud={isCloud !== true} userdata={userdata} getAppAuthentication={getAppAuthentication} /> @@ -853,35 +983,40 @@ const AppAuthTab = memo((props) => { ) : null; return ( -
+
{appModal} -
+ {cacheDistributionModal} +
-

App Authentication

-
- + App Authentication +
+ Control the authentication options for individual apps. - +   Learn more about App Authentication
- + + {isCloud ? + + : null}
{/* { style={{ borderRadius: 4, marginTop: 24, - border: "1px solid #494949", + border: theme.palette.defaultBorder, width: "100%", overflowX: "auto", paddingBottom: 0, @@ -913,7 +1048,7 @@ const AppAuthTab = memo((props) => { }}> - + {["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => ( { padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px", whiteSpace: "nowrap", textOverflow: "ellipsis", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, position: "sticky", }} /> @@ -938,7 +1073,7 @@ const AppAuthTab = memo((props) => { key={rowIndex} style={{ display: "table-row", - backgroundColor: "#212121", + backgroundColor: theme.palette.platformColor, }} > {Array(8) @@ -955,7 +1090,7 @@ const AppAuthTab = memo((props) => { variant="text" animation="wave" sx={{ - backgroundColor: "#1a1a1a", + backgroundColor: theme.palette.loaderColor, height: "20px", borderRadius: "4px", }} @@ -966,14 +1101,14 @@ const AppAuthTab = memo((props) => { )) : authentication?.length === 0 ? (
- + No authentication found.
):authentication.map((data, index) => { - var bgColor = "#212121"; + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; if (index % 2 === 0) { - bgColor = "#1A1A1A"; + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; } //console.log("Auth data: ", data) @@ -998,7 +1133,7 @@ const AppAuthTab = memo((props) => { ]; } - const isDistributed = data.suborg_distributed === true ? true : false; + const isDistributed = data?.suborg_distribution?.length > 0 || data?.suborg_distributed ? true : false; var validIcon = if (data.validation !== null && data.validation !== undefined && data.validation.valid === false) { @@ -1055,7 +1190,7 @@ const AppAuthTab = memo((props) => { - + {data?.app?.name?.replaceAll("_", " ")} @@ -1108,14 +1243,14 @@ const AppAuthTab = memo((props) => { style={{ overflow: "hidden", display: "table-cell", - verticalAlign: 'middle' + verticalAlign: 'middle', }} primaryTypographyProps={{ style: { padding: 8 } }} - primary={new Date(data.edited * 1000).toISOString()} + primary={new Date(data.edited * 1000).toISOString()} /> { primaryTypographyProps={{ style: { display: "flex", flexDirection: 'row', padding: 8 } }} > { - updateAppAuthentication(data); - }} - disabled={data.org_id !== selectedOrganization.id} - > - Edit icon - + onClick={() => updateAppAuthentication(data)} + disabled={data.org_id !== selectedOrganization.id} + > + + + + {data.defined ? ( { editAuthenticationConfig(data.id); }} > - + ) : ( @@ -1164,6 +1310,47 @@ const AppAuthTab = memo((props) => { )} + + { + navigator.clipboard.writeText(data.id); + document.execCommand("copy"); + + toast(data.id + " copied to clipboard"); + }} + > + + + + + + + { style={{ }} color="secondary" onClick={() => { - changeDistribution(data, !isDistributed) + setShowDistributionPopup(true) + if(data?.suborg_distribution?.length > 0){ + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedAuthId(data.id) + if (data?.suborg_distributed) { + const allSuborg = userdata?.orgs?.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + return data.id; + }) + setSelectedSubOrg(allSuborg.filter((data) => data !== null)) + } + }} /> @@ -1216,6 +1419,8 @@ const AppAuthTab = memo((props) => { {editAuthenticationModal} {authenticationView}
+ + {/*

App Authentication Groups

@@ -1414,13 +1619,14 @@ const AppAuthTab = memo((props) => { ); } )} - -
- -
-
-
-
+ +
+ +
+ */} +
+
+
); }); @@ -1432,6 +1638,9 @@ const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, se refine(searchQuery.trim()); }; + const { themeMode, supportEmail } = useContext(Context); + const theme = getTheme(themeMode); + return (
0 && ( { if (typeof name === 'string') { @@ -1725,7 +1934,7 @@ const Hits = ({ }) .then((response) => { if (response.status !== 200) { - toast.error("Failed to get app data or App doesn't. Please contact support@shuffler.io"); + toast.error(`Failed to get app data or App doesn't. Please contact ${supportEmail}`); return; } return response.json(); @@ -1943,7 +2152,7 @@ const Hits = ({ return (
-
+
Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
@@ -2013,7 +2222,7 @@ const Hits = ({ }} style={{ backgroundColor: theme.palette.surfaceColor, - color: "white", + color: theme.palette.textColor, height: 50, }} > @@ -2021,7 +2230,7 @@ const Hits = ({ key={"false"} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.textColor, }} value={"false"} > @@ -2031,7 +2240,7 @@ const Hits = ({ key={"true"} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.textColor, }} value={"true"} > @@ -2110,7 +2319,7 @@ const Hits = ({ PaperProps={{ style: { pointerEvents: "auto", - color: "white", + color: theme.palette.textColor, minWidth: 1100, minHeight: 700, maxHeight: 700, @@ -2392,7 +2601,7 @@ const Hits = ({ href={selectedMeta.link} style={{ textDecoration: "none", color: "#f85a3e" }} > - @@ -2403,7 +2612,7 @@ const Hits = ({ style={{ height: "100%", width: 1, - backgroundColor: "white", + backgroundColor: theme.palette.textColor, marginLeft: 50, marginRight: 50, }} @@ -2498,7 +2707,7 @@ const Hits = ({ height: 480, overflowY: "auto", scrollbarWidth: "thin", - scrollbarColor: "#494949 #2f2f2f", + scrollbarColor: theme.palette.scrollbarColor, width: "100%", }} > @@ -2529,7 +2738,7 @@ const Hits = ({ elevation={0} style={{ ...paperStyle, - backgroundColor: mouseHoverIndex === index ? "#2F2F2F" : "rgba(26, 26, 26, 1)", + backgroundColor: mouseHoverIndex === index ? theme.palette.cardHoverColor : theme.palette.cardBackgroundColor, width: "100%", }} onMouseEnter={() => setMouseHoverIndex(index)} @@ -2588,7 +2797,7 @@ const Hits = ({ gap: 8, textOverflow: "ellipsis", whiteSpace: "nowrap", - color: "#F1F1F1", + color: theme.palette.textColor, }} > {normalizedString(data.name)} @@ -2631,7 +2840,7 @@ const Hits = ({ ))}
-
diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx index f5a25169..28dea461 100644 --- a/frontend/src/components/AppCreationModal.jsx +++ b/frontend/src/components/AppCreationModal.jsx @@ -22,7 +22,6 @@ import CreateIcon from '@mui/icons-material/Create' import { toast } from 'react-toastify' import YAML from "yaml"; - const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { const [openApiModal, setOpenApiModal] = useState(false) const [generateAppModal, setGenerateAppModal] = useState(false) @@ -43,9 +42,8 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { const parsedStyle = { flex: 1, - padding: 20, + padding: "30px 20px 20px", margin: 12, - paddingTop: 30, backgroundColor: hover && !makeFancy ? theme.palette.surfaceColor : "transparent", cursor: hover ? "pointer" : "default", textAlign: "center", @@ -59,6 +57,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { : "1px solid rgba(255,255,255,0.3)", borderImage: makeFancy ? "linear-gradient(to right, #ff8544 0%, #ec517c 50%, #9c5af2 100%) 1" : "none", transition: 'all 0.2s ease-in-out', + paddingBottom: isCloud ? 0 : 175, } return ( @@ -256,26 +255,27 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { body: openApidata, credentials: "include", }) - .then((response) => { + .then((response) => { - setValidation(false); - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success) { - setAppValidation(responseJson.id); - } else { - if (responseJson.reason !== undefined) { - setOpenApiError(responseJson.reason); - } - toast("An error occurred in the response"); - } - }) - .catch((error) => { - setValidation(false); - toast(error.toString()); - setOpenApiError(error.toString()); - }); + setValidation(false); + return response.json(); + }) + .then((responseJson) => { + if (responseJson?.success === true) { + setAppValidation(responseJson?.id) + navigate(`/apps/new?id=${responseJson?.id}`) + } else { + if (responseJson.reason !== undefined) { + setOpenApiError(responseJson.reason) + } + toast("An error occurred in the response"); + } + }) + .catch((error) => { + setValidation(false); + toast(error.toString()); + setOpenApiError(error.toString()); + }); }; const redirectOpenApi = () => { @@ -345,23 +345,23 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { // Common dialog styles const dialogStyle = { borderRadius: 2, - border: "1px solid #494949", + border: theme.palette.DialogStyle.border, minWidth: '500px', fontFamily: theme?.typography?.fontFamily, - backgroundColor: "#1A1A1A", + backgroundColor: theme.palette.DialogStyle.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { - backgroundColor: "#1A1A1A", + backgroundColor: theme.palette.DialogStyle.backgroundColor, padding: '24px', fontFamily: theme?.typography?.fontFamily, }, '& .MuiDialogTitle-root': { - backgroundColor: "#1A1A1A", + backgroundColor: theme.palette.DialogStyle.backgroundColor, padding: '24px', fontFamily: theme?.typography?.fontFamily, }, '& .MuiDialogActions-root': { - backgroundColor: "#1A1A1A", + backgroundColor: theme.palette.DialogStyle.backgroundColor, padding: '16px 24px', fontFamily: theme?.typography?.fontFamily, }, @@ -396,7 +396,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { pl: 4, pr: 3, }}> - + Create New App { onClose() }} sx={{ - color: 'rgba(255, 255, 255, 0.7)', - '&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' } + color: theme.palette.text.primary, + '&:hover': { bgcolor: theme.palette.hoverColor }, }} > -
+
{ @@ -476,7 +476,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { px: 4, }}> @@ -501,7 +501,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
- + Paste in the URI for the OpenAPI or find out { py: 1, '&:hover': { borderColor: '#FF8544', + color: '#FF8544', bgcolor: 'rgba(255,133,68,0.1)' }, textTransform: 'none', @@ -664,7 +665,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { px: 4, }}> @@ -680,8 +681,8 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { setValidation(false) }} sx={{ - color: 'rgba(255,255,255,0.7)', - '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } + color: theme.palette.text.primary, + '&:hover': { bgcolor: theme.palette.hoverColor } }} > @@ -689,7 +690,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { { variant="outlined" placeholder="API Documentation URL" sx={{ - bgcolor: theme.palette.platformColor, + bgcolor: theme.palette.textFieldStyle.backgroundColor, '& .MuiOutlinedInput-root': { height: '40px', - color: 'white', + color: theme.palette.text.primary, '& fieldset': { borderWidth: '1px', borderImage: "linear-gradient(to right, #ff8544 0%, #ec517c 50%, #9c5af2 100%) 1", @@ -756,7 +757,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { {circularLoader} { !validation && - + This may take multiple minutes based on the size of the documentation. } diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx index c65cf1cf..d6fc1f42 100644 --- a/frontend/src/components/AppFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -198,7 +198,7 @@ export const findSpecificApp = (framework, inputcategory) => { id: "", } } else { - console.log("findSpecificApp: unknown category: ", category) + //console.log("findSpecificApp: unknown category: ", category) } return null diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index 8a5d9a18..e647085b 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -53,7 +53,7 @@ import { const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ); //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") diff --git a/frontend/src/components/AppModal.jsx b/frontend/src/components/AppModal.jsx index c3deb1ae..38c8581f 100644 --- a/frontend/src/components/AppModal.jsx +++ b/frontend/src/components/AppModal.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { memo, useCallback, useEffect, useState, useContext } from 'react'; import { useNavigate } from 'react-router'; import { @@ -25,12 +25,20 @@ import LaunchIcon from '@mui/icons-material/Launch'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import { CloudDownloadOutlined, Delete } from '@mui/icons-material'; import { findSpecificApp } from '../components/AppFramework.jsx'; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import YAML from 'yaml'; import { toast } from 'react-toastify'; import { Link } from 'react-router-dom'; +import { InstantSearch, connectHits, connectSearchBox } from 'react-instantsearch-dom'; +import algoliasearch from "algoliasearch/lite"; +import { Context } from '../context/ContextApi.jsx'; -const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "c8f882473ff42d41158430be09ec2b4e" +);; + +const AppModal = ({ open, onClose, app, globalUrl, getApps}) => { const [frameworkData, setFrameworkData] = useState({}) const [userdata, setUserdata] = useState({}) @@ -44,6 +52,9 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { const [deleteModalOpen, setDeleteModalOpen] = useState(false) const [sharingConfiguration, setSharingConfiguration] = React.useState("you"); const navigate = useNavigate(); + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode); + const parseUsecase = (subcase) => { const srcdata = findSpecificApp(frameworkData, subcase.type) const dstdata = findSpecificApp(frameworkData, subcase.last) @@ -79,8 +90,7 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { }); }, [app]); - - const getFramework = () => { + const getFramework = () => { fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { method: "GET", headers: { @@ -295,8 +305,6 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { }) } - - useEffect(() => { setUsecaseLoading(true) getAvailableWorkflows() @@ -329,6 +337,7 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { //delete apps from local storage localStorage.removeItem("apps"); getApps(); + onClose(); }, 1000); } else { toast("Failed deleting app. Does it still exist?"); @@ -511,12 +520,12 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { border: "1px solid #494949", minWidth: '440px', fontFamily: theme?.typography?.fontFamily, - backgroundColor: "#212121", + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, '& .MuiDialogContent-root': { - backgroundColor: "#212121", + backgroundColor: theme.palette.DialogStyle.backgroundColor, }, '& .MuiDialogTitle-root': { - backgroundColor: "#212121", + backgroundColor: theme.palette.DialogStyle.backgroundColor, }, '& .MuiTypography-root': { fontFamily: theme?.typography?.fontFamily, @@ -548,7 +557,7 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { { >
{top_text === "Base Cloud Access" && userdata.has_card_available === true ? { { }} @@ -857,6 +886,7 @@ const Billing = memo((props) => { "Add Card Details" } + {userdata.has_card_available === true ? - : null} + : null} */}
) } @@ -1061,12 +1092,12 @@ const Billing = memo((props) => { padding: 20, // maxWidth: 400, width: "100%", - height: 480, - backgroundColor: hovered ? "#2b2b2b" : "#1e1e1e", + height: 500, + backgroundColor: hovered ? theme.palette.cardHoverColor : theme.palette.cardBackgroundColor, borderRadius: theme.palette?.borderRadius * 2, border: "1px solid rgba(255,255,255,0.3)", marginRight: 10, - marginTop: 15, + marginTop: 15, }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}> @@ -1133,12 +1164,12 @@ const Billing = memo((props) => { Features
    -
  • +
  • Build custom apps, integrations, and worklows for your specific use cases or applications
  • -
  • +
  • Help solve / debug / update / add features and capabilities of the platform @@ -1149,15 +1180,13 @@ const Billing = memo((props) => { )} { Cancel
- + + { + setCurrentTab(-1) + + // Force re-render + setTimeout(() => { + setCurrentTab(newValue) + }, 100); + }} + style={{ marginTop: 20 }} + TabIndicatorProps={{ + style: { + height: 3, + backgroundColor: theme.palette.primary.main, + marginLeft: 12, + marginRight: 12, + } + }} + > + + + {isCloud ? + + : null} + + + + +
+ {currentTab === 0 ? +
+ +
+ : currentTab === 1 ? +
+ +
+ : + + } +
+
) @@ -2569,19 +2687,473 @@ const Billing = memo((props) => { export default memo(Billing); -const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => { +const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, allChildOrgs, setAllChildOrgs, allChildOrgsStats, setAllChildOrgsStats }) => { + const [subOrgStats, setSubOrgStats] = useState([]); + const [subOrgs, setSubOrgs] = useState([]); + const [subOrgStatsRows, setSubOrgStatsRows] = useState([]); + const [subOrgStatsColumns, setSubOrgStatsColumns] = useState([]); + const [allOrgLoaded, setAllOrgLoaded] = useState(false); + const [allOrgStatsLoaded, setAllOrgStatsLoaded] = useState(false); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [open, setOpen] = useState(false); + const [editing, setEditing] = useState("") + const [editingOrgId, setEditingOrgId] = useState("") + const [limit, setLimit] = useState("") + const [tableCreated, setTableCreated] = useState(false) + const [searchQuery, setSearchQuery] = useState(""); + const [filteredRows, setFilteredRows] = useState([]); + const { themeMode, brandColor, supportEmail } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + + // Handle page change + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const HanldeLoadStats = async () => { + const childOrgs = selectedOrganization.child_orgs; + if (allChildOrgsStats.length > 0){ + setSubOrgStats(allChildOrgsStats) + setAllOrgStatsLoaded(true) + if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) { + HandleCreateTable(allChildOrgsStats, allChildOrgs) + } + return + } + const promises = childOrgs.map((org) => { + // get org stats base on region url + const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl; + const url = `${baseUrl}/api/v1/orgs/${org.id}/stats`; + return fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }).then((res) => res.json()); + }); + + try { + const responses = await Promise.all(promises); + setSubOrgStats(responses); + setAllChildOrgsStats(responses); + setAllOrgStatsLoaded(true); + } catch (error) { + console.error("Error loading stats:", error); + } + }; + + + + const HandleGetSuborg = async () => { + const childOrgs = selectedOrganization.child_orgs + + if (allChildOrgs.length > 0){ + setSubOrgs(allChildOrgs) + setAllOrgLoaded(true) + if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) { + HandleCreateTable(allChildOrgsStats, allChildOrgs) + } + return + } + + const promises = childOrgs.map((org) => { + const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl; + const url = `${baseUrl}/api/v1/orgs/${org.id}`; + return fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }).then((res) => res.json()); + }); + try { + const responses = await Promise.all(promises); + setSubOrgs(responses); + setAllChildOrgs(responses); + setAllOrgLoaded(true); + } catch (error) { + console.error("Error loading suborgs:", error); + } + }; + + useEffect(() => { + if (subOrgStats.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) { + HanldeLoadStats() + } + if (subOrgs.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) { + HandleGetSuborg() + } + + }, [selectedOrganization?.child_orgs]); + + + useEffect(() => { + + if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) { + HandleCreateTable(subOrgStats, subOrgs) + } + } + , [allOrgLoaded, allOrgStatsLoaded, tableCreated]) + + + const HandleCreateTable = (subOrgStats, subOrgs) => { + + if (subOrgStats.length === 0 || subOrgs.length === 0) return; + + // check whether all of the suborg.success is false + const allSubOrgStatsSuccess = subOrgStats.every((stat) => stat.success === false); + const allSubOrgsSuccess = subOrgs.every((org) => org.success === false); + + if (allSubOrgStatsSuccess || allSubOrgsSuccess) { + setSubOrgStats([]) + setSubOrgs([]) + setTableCreated(true) + return + } + + const rows = subOrgStats.map((stat, index) => { + const subOrg = subOrgs[index] + if (!subOrg) return null; + return { + id: index, + name: subOrg.name, + orgId: subOrg.id, + limit: subOrg?.sync_features?.app_executions?.limit || "N/A", + usage: stat?.monthly_app_executions || "N/A", + workflows_usage: stat?.total_workflow_executions || "N/A", + workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A", + } + }) + + setSubOrgStatsRows(rows) + + const columns = [ + { field: "id", headerName: "ID", width: 100 }, + { field: "name", headerName: "Name", width: 200 }, + { field: "usage", headerName: "App Execution Usage", width: 200 }, + { + field: "limit", headerName: "App Execution Limit", width: 200, renderCell: (params) => { + return ( + <> + + {params.value} + { + setOpen(true) + setEditingOrgId(params.row.orgId) + setEditing("app_executions") + if (params.value === "N/A") { + setLimit("") + } else { + setLimit(params.value) + } + }} + > + + + + + ) + } + }, + { field: "workflows_usage", headerName: "Workflow Execution Usage", width: 200 }, + { field: "workflow_usage_limit", headerName: "Workflow Execution Limit", width: 200, renderCell: (params) => { + return ( + <> + + {params.value} + + { + setOpen(true) + setEditingOrgId(params.row.orgId) + setEditing("workflow_executions") + if (params.value === "N/A") { + setLimit("") + } else { + setLimit(params.value) + } + }} + > + + + + )} + }, + ] + + setSubOrgStatsColumns(columns) + + if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) { + setTableCreated(true) + } + } + + + + const HandleEditLimit = (orgId, editing, limit) => { + + + // change limit as number if string + if (typeof limit === "string") { + limit = parseInt(limit, 10) + } + if (isNaN(limit)) { + toast.error("Please enter a valid number") + return + } + + if (selectedOrganization.sync_features.app_executions.limit <= 10000) { + toast.error("Insufficient app execution limit to increase child org limit") + return + } + + // check whether limit is greater than than parent org limit + if (editing === "app_executions" && limit > selectedOrganization.sync_features.app_executions.limit && !userdata.support) { + toast.error("App execution limit cannot be greater than parent org limit") + return + } + + + if (editing === "workflow_executions" && limit > selectedOrganization.sync_features.workflow_executions.limit && !userdata.support) { + toast.error("Workflow execution limit cannot be greater than parent org limit") + return + } + + // find the org in the subOrgs array + const orgIndex = subOrgs.findIndex((org) => org.id === orgId) + if (orgIndex === -1) { + toast.error("Organization not found") + return + } + + const org = subOrgs[orgIndex] + + org.sync_features[editing].limit = limit + org.sync_features.editing = true + const sync_features = org.sync_features + const data = { + org_id: orgId, + sync_features: sync_features, + } + + const url = `${globalUrl}/api/v1/orgs/${orgId}`; + fetch(url, { + method: "POST", + credentials: "include", + crossDomain: true, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully change suborg limit!"); + if (editing === "app_executions") { + setSubOrgStatsRows((prevRows) => { + const newRows = [...prevRows]; + newRows[orgIndex].limit = limit; + return newRows; + }); + }else if (editing === "workflow_executions") { + setSubOrgStatsRows((prevRows) => { + const newRows = [...prevRows]; + newRows[orgIndex].workflow_usage_limit = limit; + return newRows; + }); + } + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + + } + + const HandleClosePopUP = () => { + setOpen(false) + setEditing("") + setEditingOrgId("") + setLimit("") + } + + return ( +
+ {open && ( + + )} + + Child Organizations + +
+ + View and configure execution limits for child organizations. Click the edit icon to modify app and workflow execution limits. + +
+ {tableCreated ? ( + subOrgStatsRows.length > 0 && subOrgStatsColumns.length > 0 ? ( + <> + + + + ), + }} + onChange={(e) => { + setSearchQuery(e.target.value.toLowerCase()); + const filtered = subOrgStatsRows.filter((row) => + row.name.toLowerCase().includes(e.target.value.toLowerCase().trim()) || + row.orgId.toLowerCase().includes(e.target.value.toLowerCase().trim()) + ); + setFilteredRows(filtered); + }} + /> + + + ) : ( + + {selectedOrganization.child_orgs.length === 0 ? "No child organizations exist." : "Unable to load child organization stats. Statistics may not be initialized yet." } + + ) + ) : ( +
+ +
+ )} +
+ ); +}); + +const IncreaseLimitPopUp = memo(({ open, onClose, limit, setLimit, HandleEditLimit, editingOrgId, editing}) => { + const [currentLimit, setCurrentLimit] = useState(limit) + + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + + return( + + + Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit + + + setCurrentLimit(e.target.value)} + label={`${editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit`} + type="string" + variant="outlined" + fullWidth + InputProps={{ + style: { + color: theme.palette.text.primary, + }, + }} + InputLabelProps={{ + style: { + color: theme.palette.text.primary, + }, + }} + margin="normal" + onKeyUp={(e) => { + if (e.key === "Enter") { + HandleEditLimit(editingOrgId, editing, currentLimit) + setLimit(currentLimit) + onClose() + }} + } + > + + + + + + + ) +}) + + +const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => { + + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const wrapperStyle = useMemo(() => ({ width: clickedFromOrgTab ? "100%" : "auto", padding: "27px 10px 19px 27px", - backgroundColor: '#212121', + backgroundColor: theme.palette.platformColor, height: '100%', boxSizing: 'border-box', overflow: 'hidden', - maxHeight: "1700px", overflowY: "auto",scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' - }), [clickedFromOrgTab]); + maxHeight: "1700px", + overflowY: "auto", + scrollbarColor: theme.palette.scrollbarColorTransparent, + scrollbarWidth: 'thin' + }), [clickedFromOrgTab, theme]); return (
@@ -2590,9 +3162,9 @@ const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => { ); }); - const Wrapper = memo(({ children, clickedFromOrgTab }) => { +const Wrapper = memo(({ children, clickedFromOrgTab }) => { return ( - + {children} ); diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 231b5686..5bb09ddb 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useContext, memo, useMemo } from 'react'; -import theme from '../theme.jsx'; +import {getTheme} from '../theme.jsx'; import classNames from "classnames"; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid' @@ -28,12 +28,20 @@ import { Paper, Chip, Checkbox, + Box, } from "@mui/material"; import { BarChart, + BarSeries, + Bar, + BarLabel, + GridlineSeries, Gridline, + TooltipArea, + ChartTooltip, + TooltipTemplate, } from 'reaviz'; import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; @@ -42,31 +50,54 @@ import { Context } from '../context/ContextApi.jsx'; const LineChartWrapper = ({keys, inputname, height, width}) => { const [hovered, setHovered] = useState(""); const inputdata = keys.data === undefined ? keys : keys.data - + const {themeMode} = useContext(Context) + const theme = getTheme(themeMode) + + return (
- + {inputname} + + } + /> + } gridlines={ } /> } /> +
) } const AppStats = (defaultprops) => { - const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows,clickedFromOrgTab } = defaultprops; + const { + globalUrl, + selectedOrganization, + userdata, + isCloud, + inputWorkflows, + clickedFromOrgTab, + syncStats, + } = defaultprops; const [keys, setKeys] = useState([]) const [searches, setSearches] = useState([]); const [appRuns, setAppruns] = useState(undefined); + const [childOrgsAppRuns, setChildOrgsAppRuns] = useState(undefined); const [appRunCosts, setApprunCosts] = useState(undefined); const [workflowRuns, setWorkflowRuns] = useState(undefined); const [subflowRuns, setSubflowRuns] = useState(undefined); @@ -83,6 +114,8 @@ const AppStats = (defaultprops) => { const [workflows, setWorkflows] = useState(inputWorkflows === undefined ? [] : inputWorkflows) const [resultRows, setResultRows] = useState([]) const [resultLoading, setResultLoading] = useState(true) + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor) const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0 @@ -94,9 +127,6 @@ const AppStats = (defaultprops) => { const getWorkflowStats = async (workflow, startTime, endTime) => { - if (!userdata.support) { - return workflow - } if (workflow.id === undefined || workflow.id === null || workflow.id === "") { return workflow @@ -161,12 +191,8 @@ const AppStats = (defaultprops) => { } const loadWorkflowStats = (foundWorkflows, startTime, endTime) => { - if (!userdata.support) { - return - } - if (foundWorkflows === undefined || foundWorkflows === null || foundWorkflows.length === 0) { - console.log("Not workflows") + setResultLoading(false) return } @@ -175,6 +201,9 @@ const AppStats = (defaultprops) => { const promises = foundWorkflows.slice(0, 50).map(wf => getWorkflowStats(wf, startTime, endTime)); const allData = Promise.all(promises); + if (allData === undefined || allData === null) { + setResultLoading(false) + } allData.then((data) => { var total = 0 @@ -234,15 +263,16 @@ const AppStats = (defaultprops) => { return } - if (statistics["daily_statistics"] === undefined || statistics["daily_statistics"] === null) { + const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" + if (statistics[statKey] === undefined || statistics[statKey] === null) { setFilteredStatistics(statistics) return } // Calculate month to date cost var mtd_cost = 0 - for (let key in statistics["daily_statistics"]) { - const item = statistics["daily_statistics"][key] + for (let key in statistics[statKey]) { + const item = statistics[statKey][key] if (item["date"] === undefined) { continue } @@ -300,8 +330,8 @@ const AppStats = (defaultprops) => { // Check if start time is before the daily statistics["date"] string var newlist = [] - for (let key in statistics["daily_statistics"]) { - const item = statistics["daily_statistics"][key] + for (let key in statistics[statKey]) { + const item = statistics[statKey][key] if (item["date"] === undefined) { continue } @@ -332,7 +362,7 @@ const AppStats = (defaultprops) => { var appexecutions = 0 var estimatedcost = 0 if (newlist.length > 0) { - tmpstats["daily_statistics"] = newlist + tmpstats[statKey] = newlist for (let key in newlist) { const item = newlist[key] @@ -386,7 +416,8 @@ const AppStats = (defaultprops) => { return } - const dailyStats = inputdata.daily_statistics + const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" + const dailyStats = inputdata[statKey] if (dailyStats === undefined || dailyStats === null) { return } @@ -396,6 +427,11 @@ const AppStats = (defaultprops) => { "data": [] } + var childorgappRuns = { + "key": "Child Org App Runs", + "data": [] + } + var workflowRuns = { "key": "Workflow Runs (includes subflows)", "data": [] @@ -437,6 +473,13 @@ const AppStats = (defaultprops) => { }) } + if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) { + childorgappRuns["data"].push({ + key: new Date(item["date"]), + data: inputdata["child_app_executions"] + }) + } + // Check if workflow_executions key in item if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) { workflowRuns["data"].push({ @@ -466,6 +509,15 @@ const AppStats = (defaultprops) => { }) } + if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { + childorgappRuns["data"].push({ + key: new Date(), + data: inputdata["daily_child_app_executions"] + }) + + //setApprunCosts(appcostRuns) + } + if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { workflowRuns["data"].push({ key: new Date(), @@ -480,6 +532,11 @@ const AppStats = (defaultprops) => { }) } + // Only for parent orgs + if (childorgappRuns["data"].length > 0) { + setChildOrgsAppRuns(childorgappRuns) + } + setSubflowRuns(subflowRuns) setWorkflowRuns(workflowRuns) setAppruns(appRuns) @@ -529,11 +586,14 @@ const AppStats = (defaultprops) => { const paperStyle = { textAlign: "center", - padding: 40, - margin: 5, - backgroundColor: theme.palette.platformColor, - border: "1px solid rgba(255,255,255,0.3)", - maxWidth: 300, + padding: "40px", + margin: "5px", + backgroundColor: theme.palette.cardBackgroundColor, + border: theme.palette.defaultBorder, + maxWidth: "300px", + "&:hover": { + backgroundColor: theme.palette.cardHoverColor, + }, } const columns: GridColDef[] = [ @@ -646,85 +706,94 @@ const AppStats = (defaultprops) => {
All shown statistics are gathered from Your Organisation Statistics. It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. + +
+ {syncStats !== true ? null : + "PS: You are currently looking at data from your onprem synced org"}
{filteredStatistics !== undefined ?
- - The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. - - }> - - - ${selectedOrganization.lead_info.customer === false && selectedOrganization.lead_info.pov === false ? - 0 - : - apprunCost - } + + {syncStats == true ? null : + + The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. - - Period Cost - - - + }> + + + ${selectedOrganization?.lead_info?.customer === false && selectedOrganization?.lead_info?.pov === false ? + 0 + : + apprunCost + } + + + + Period Cost + + + + } + + {syncStats === true ? null : App runs in the selected period }> - - - {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} - - - App Runs - - + + + {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} + + + App Runs + + + } + + {syncStats === true ? null : Workflow runs in the selected period }> - + {filteredStatistics.monthly_workflow_executions === null || filteredStatistics.monthly_workflow_executions === undefined ? 0 : filteredStatistics.monthly_workflow_executions} Workflow Runs - + - - Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. - - }> - - - ${monthTotalCost} + } + + {syncStats === true ? null : + + Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. - - Estimated cost - - - + }> + + + ${monthTotalCost} + + + Estimated cost + + + + }
: null}
@@ -895,7 +964,13 @@ const AppStats = (defaultprops) => { {appRuns === undefined ? null : - + + } + + {childOrgsAppRuns === undefined ? + null + : + } {workflowRuns === undefined ? @@ -916,56 +991,58 @@ const AppStats = (defaultprops) => { */} + {syncStats === true ? null : +
+ {resultLoading ? +
+ + Loading usage for selected period (may take a while) + + + +
+ : + { + //setRowsPerPage(newPageSize) + //submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize) + }} + // event for when clicking next page + // Hide page changer + onPageChange={(params) => { + console.log("page params: ", params) + }} + onSelectionModelChange={(newSelection) => { + console.log("newSelection: ", newSelection) + //console.log("newSelection: ", newSelection) + //setSelectedWorkflowExecutionsIndexes(newSelection) + //var found = [] + //for (var i = 0; i < newSelection.length; i++) { + // // Find the workflow in the resultRows + // var selected = resultRows.find((workflow) => { + // return workflow.id === newSelection[i] + // }) -
- {resultLoading ? -
- - Loading usage for selected period (may take a while) - - -
- : - { - //setRowsPerPage(newPageSize) - //submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize) - }} - // event for when clicking next page - // Hide page changer - onPageChange={(params) => { - console.log("page params: ", params) - }} - onSelectionModelChange={(newSelection) => { - console.log("newSelection: ", newSelection) - //console.log("newSelection: ", newSelection) - //setSelectedWorkflowExecutionsIndexes(newSelection) - //var found = [] - //for (var i = 0; i < newSelection.length; i++) { - // // Find the workflow in the resultRows - // var selected = resultRows.find((workflow) => { - // return workflow.id === newSelection[i] - // }) + // if (selected === undefined || selected === null) { + // continue + // } - // if (selected === undefined || selected === null) { - // continue - // } + // found.push(selected) + //} - // found.push(selected) - //} - - //setSelectedWorkflowExecutions(found) - }} - // Track which items are selected - /> - } -
+ //setSelectedWorkflowExecutions(found) + }} + // Track which items are selected + /> + } +
+ }
) diff --git a/frontend/src/components/Branding.jsx b/frontend/src/components/Branding.jsx index e549d12e..f913a58a 100644 --- a/frontend/src/components/Branding.jsx +++ b/frontend/src/components/Branding.jsx @@ -1,8 +1,7 @@ import React, { useState, useEffect, useContext } from "react"; import ReactGA from 'react-ga4'; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import { ToastContainer, toast } from "react-toastify" - import { CheckCircle as CheckCircleIcon, } from "@mui/icons-material"; @@ -13,8 +12,11 @@ import { Divider, Button, Tooltip, - Grid, - Card, + ToggleButtonGroup, + ToggleButton, + useMediaQuery, + TextField, + Box, } from "@mui/material"; import { @@ -27,18 +29,98 @@ import { Context } from "../context/ContextApi.jsx"; const Branding = (props) => { const { globalUrl, userdata, serverside, billingInfo,clickedFromOrgTab, stripeKey, selectedOrganization, handleGetOrg, } = props; + const { themeMode, handleThemeChange, supportEmail, setSupportEmail, logoutUrl, setLogoutUrl, brandColor, setBrandColor, setBrandName } = useContext(Context) //const alert = useAlert(); const [publishingInfo, setPublishingInfo] = useState(""); const [publishRequirements, setPublishRequirements] = useState([]) + const [currentSelectedTheme, setCurrentSelectedTheme] = useState(themeMode); + const [integrationPartner, setIntegrationPartner] = useState(false); + const [changingTheme, setChangingTheme] = useState(false); + const theme = getTheme(themeMode, brandColor) + const [selectedBrandColor, setSelectedBrandColor] = useState(theme?.palette?.main || "#FF8544") + const [selectedBrandName, setSelectedBrandName] = useState(selectedOrganization?.branding?.brand_name || "") - const { leftSideBarOpenByClick } = useContext(Context) - - const handleEditOrg = (joinStatus) => { + const [isLoading,setIsLoading] = useState(false); + + const handleEditOrg = (joinStatus) => { + setIsLoading(true) const data = { "org_id": selectedOrganization.id, - "creator_config": joinStatus, }; + if (joinStatus === "join" || joinStatus === "leave") { + data["creator_config"] = joinStatus + } + + if (joinStatus === "light" || joinStatus === "dark" || joinStatus === "system") { + data["branding"] = { + "theme": joinStatus, + "enable_chat": selectedOrganization?.branding?.enable_chat || false, + "home_url": selectedOrganization?.branding?.home_url || "", + "brand_color": selectedOrganization?.branding?.brand_color || theme.palette.primary.main, + "brand_name": selectedOrganization?.branding?.brand_name || "", + "logout_url": selectedOrganization?.branding?.logout_url || "", + "support_email": selectedOrganization?.branding?.support_email || "", + } + + data["editing_branding"] = true; + } + + if (joinStatus === "brand_color") { + data["branding"] = { + "theme": selectedOrganization?.branding?.theme || "dark", + "enable_chat": selectedOrganization?.branding?.enable_chat || false, + "home_url": selectedOrganization?.branding?.home_url || "", + "brand_color": selectedBrandColor, + "brand_name": selectedOrganization?.branding?.brand_name || "", + "logout_url": selectedOrganization?.branding?.logout_url || "", + "support_email": selectedOrganization?.branding?.support_email || "", + } + + data["editing_branding"] = true; + } + + if (joinStatus === "brand_name") { + data["branding"] = { + "theme": selectedOrganization?.branding?.theme || "dark", + "enable_chat": selectedOrganization?.branding?.enable_chat || false, + "home_url": selectedOrganization?.branding?.home_url || "", + "brand_color": selectedOrganization?.branding?.brand_color || theme.palette.primary.main, + "brand_name": selectedBrandName, + "logout_url": selectedOrganization?.branding?.logout_url || "", + "support_email": selectedOrganization?.branding?.support_email || "", + } + toast.info("Updating brand name to " + selectedBrandName + ". Please wait a moment.") + data["editing_branding"] = true; + } + + if (joinStatus === "support_email") { + data["branding"] = { + "theme": selectedOrganization?.branding?.theme || "dark", + "enable_chat": selectedOrganization?.branding?.enable_chat || false, + "home_url": selectedOrganization?.branding?.home_url || "", + "brand_color": selectedOrganization?.branding?.brand_color || theme.palette.primary.main, + "brand_name": selectedOrganization?.branding?.brand_name || "", + "support_email": supportEmail, + "logout_url": selectedOrganization?.branding?.logout_url || "", + } + data["editing_branding"] = true; + } + + if (joinStatus === "logout_url") { + data["branding"] = { + "theme": selectedOrganization?.branding?.theme || "dark", + "enable_chat": selectedOrganization?.branding?.enable_chat || false, + "home_url": selectedOrganization?.branding?.home_url || "", + "brand_color": selectedOrganization?.branding?.brand_color || theme.palette.primary.main, + "brand_name": selectedOrganization?.branding?.brand_name || "", + "support_email": selectedOrganization?.branding?.support_email || "", + "logout_url": logoutUrl, + } + data["editing_branding"] = true; + } + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; fetch(url, { mode: "cors", @@ -56,20 +138,65 @@ const Branding = (props) => { if (responseJson["success"] === false) { toast("Failed updating org: ", responseJson.reason); } else { - if (joinStatus == "join") { - setPublishingInfo("Your organization is now part of the Partner Program. You can now create, publish and manage content for your organization's public page.") - } else { - setPublishingInfo("Your organization is no longer part of the Creator Incentive Program. You can still create a creator account to manage your organization's content.") + if (joinStatus === "join" || joinStatus === "leave") { + if (joinStatus === "join") { + setPublishingInfo("Your organization is now part of the Partner Program. You can now create, publish and manage content for your organization's public page.") + } else { + setPublishingInfo("Your organization is no longer part of the Creator Incentive Program. You can still create a creator account to manage your organization's content.") + } } + + if (joinStatus === "light" || joinStatus === "dark" || joinStatus === "system") { + handleThemeChange(joinStatus) + setChangingTheme(false) + setCurrentSelectedTheme(joinStatus) + } + + if (joinStatus === "support_email") { + toast.info("Support email updated successfully.") + if (supportEmail?.length > 0) { + setSupportEmail(supportEmail) + }else { + setSupportEmail("support@shuffler.io") + } + } + + if (joinStatus === "logout_url") { + toast.info("Logout URL updated successfully.") + setLogoutUrl(logoutUrl) + } + + if (joinStatus === "brand_color") { + toast.info("Brand color updated successfully.") + setBrandColor(selectedBrandColor) + localStorage.setItem("brandColor", selectedBrandColor) + } + + if (joinStatus === "brand_name") { + toast.info("Brand name updated successfully.") + setBrandName(selectedBrandName) + localStorage.setItem("brandName", selectedBrandName) + } + handleGetOrg(selectedOrganization.id); - } + + setIsLoading(false) + } }) ) .catch((error) => { toast("Err: " + error.toString()); - }); + setIsLoading(false) + }) }; + useEffect(() => { + if (userdata && userdata?.active_org && userdata?.active_org?.branding?.theme?.length > 0) { + console.log("Setting current selected theme from userdata", userdata?.active_org?.branding?.theme); + setCurrentSelectedTheme(userdata?.active_org?.branding?.theme); + } + },[userdata]); + // Should enable / disable org branding const handleChangePublishing = () => { console.log("Handle change publishing"); @@ -117,16 +244,56 @@ const Branding = (props) => { const leadinfo = selectedOrganization.lead_info === undefined || selectedOrganization.lead_info === null || selectedOrganization.lead_info === "" ? "" : JSON.stringify(selectedOrganization.lead_info) const isPartner = leadinfo.includes("partner") + + useEffect(() => { + if (selectedOrganization?.branding?.theme && selectedOrganization?.creator_org?.length === 0) { + setCurrentSelectedTheme(selectedOrganization.branding.theme); + } + + if (selectedOrganization?.creator_org?.length > 0 && userdata?.active_org?.branding.theme) { + setCurrentSelectedTheme(userdata?.active_org?.branding.theme); + } + + if ( + selectedOrganization && + selectedOrganization?.branding?.brand_color && + selectedOrganization?.branding?.brand_color !== selectedBrandColor + ) { + setSelectedBrandColor(selectedOrganization.branding.brand_color); + } + + if (selectedOrganization?.branding?.brand_name && selectedOrganization?.branding?.brand_name !== selectedBrandName) { + setSelectedBrandName(selectedOrganization.branding.brand_name); + } + + }, [selectedOrganization, userdata]); + + useEffect(() => { + if ((userdata && userdata?.org_status?.includes("integration_partner") && !integrationPartner && !userdata?.org_status?.includes("sub_org")) || userdata?.support) { + setIntegrationPartner(true) + } + + },[userdata, integrationPartner]); + + const handleColorChange = (e) => { + setSelectedBrandColor(e.target.value); + }; + + const saveColorChanges = () => { + toast.info("Updating brand color to " + selectedBrandColor + ". Please wait a moment.") + handleEditOrg("brand_color"); + }; + return ( -
-
+
+
Partner Status & Branding - You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more. + Please note that same theme settings are applied to all sub organizations for partners. @@ -150,7 +317,8 @@ const Branding = (props) => { style={{ textDecoration: "none" }} // Optional: remove underline > )} - + - Partner Program + Public Partner Program
- - By changing publishing settings, you agree to our Terms of Service, and acknowledge that your organization's non-sensitive data will be added as a creator account. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization IS reversible.
Support: support@shuffler.io + + By changing publishing settings, you agree to our Terms of Service, and acknowledge that your organization's non-sensitive data will be added as a creator account. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization IS reversible.
Support: {supportEmail} {selectedOrganization.creator_id == "" ? @@ -195,7 +363,9 @@ const Branding = (props) => { }
+ + + {integrationPartner ? ( + <> + + + Parent & Sub Organization Branding + + + Sub organizations are not allowed to change their branding. The branding is inherited from the parent organization. + + { + if (newTheme === null) { + return; + } + if (newTheme === currentSelectedTheme) { + return; + } + if (changingTheme === true) { + return + } + setChangingTheme(true) + handleEditOrg(newTheme); + }} + aria-label="theme" + style={{ justifyContent: "flex-start", marginBottom: 10, marginTop: 20 }} + > + + Light + + + Dark + + + System + + + + ): null} + + {integrationPartner ? <> + + Brand Name +
+ { + const value = e.target.value; + setSelectedBrandName(value); + }} + size="small" + PaperProps={{ style: { backgroundColor: theme.palette.backgroundColor, borderRadius: 4, border: `1px solid ${theme.palette.defaultBorder}` } }} + style={{ + height: 36, + borderRadius: 4, + border: `1px solid ${theme.palette.defaultBorder}`, + width: 300 + }} + /> + +
+ : null} + + {integrationPartner ? <> + Brand Color +
+ + + +
+ : null} + + + {integrationPartner ? ( +
+ Support Email + + { + setSupportEmail(e.target.value) + }} + color="primary" + InputProps={{ + style: { + color: theme.palette.textFieldStyle.color, + height: "35px", + fontSize: "1em", + borderRadius: 4, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + }, + }} + /> + + +
+ ) : null} + + {integrationPartner ? ( +
+ Logout URL + + { + setLogoutUrl(e.target.value) + }} + color="primary" + InputProps={{ + style: { + color: theme.palette.textFieldStyle.color, + height: "35px", + fontSize: "1em", + borderRadius: 4, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + }, + }} + /> + + +
+ ) : null}
) diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 5ff867d6..1e94d131 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -1,8 +1,12 @@ import React, { useState, useEffect, useContext, memo } from "react"; -import theme from "../theme.jsx"; +import { makeStyles } from "@mui/styles"; +import { getTheme } from "../theme.jsx"; import { toast } from 'react-toastify'; -import ReactJson from "react-json-view-ssr"; +import ReactJson from "react-json-view-ssr"; +import { GetIconInfo } from "../views/Workflows2.jsx"; +import { red } from "../views/AngularWorkflow.jsx"; +import CollectIngestModal from "../components/CollectIngestModal.jsx"; import { Typography, Tooltip, @@ -11,7 +15,6 @@ import { Button, Tabs, Tab, - Grid, List, ListItem, ListItemText, @@ -26,8 +29,18 @@ import { DialogContent, FormControl, Select, + Autocomplete, + ButtonGroup, + InputLabel, + Pagination, + PaginationItem, } from "@mui/material"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; + import { Link as LinkIcon, AutoFixHigh as AutoFixHighIcon, @@ -35,6 +48,7 @@ import { Edit as EditIcon, FileCopy as FileCopyIcon, SelectAll as SelectAllIcon, + DeleteOutline as DeleteOutlineIcon, OpenInNew as OpenInNewIcon, CloudDownload as CloudDownloadIcon, Description as DescriptionIcon, @@ -43,7 +57,6 @@ import { Close as CloseIcon, Apps as AppsIcon, Image as ImageIcon, - Delete as DeleteIcon, Cached as CachedIcon, AccessibilityNew as AccessibilityNewIcon, Lock as LockIcon, @@ -55,6 +68,14 @@ import { VisibilityOff as VisibilityOffIcon, Clear as ClearIcon, Add as AddIcon, + Rocket as RocketIcon, + Webhook as WebhookIcon, + Air as AirIcon, + RocketLaunch as RocketLaunchIcon, + Send as SendIcon, + SmartToy as SmartToyIcon, + Settings as SettingsIcon, + FilterAlt as FilterAltIcon, } from "@mui/icons-material"; import { validateJson, } from "../views/Workflows.jsx"; import { Context } from "../context/ContextApi.jsx"; @@ -75,7 +96,15 @@ const scrollStyle2 = { overflow: "scroll", } +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); +// + +//const CacheView = (props) => { const CacheView = memo((props) => { const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props; const [orgCache, setOrgCache] = React.useState(""); @@ -86,7 +115,6 @@ const CacheView = memo((props) => { const [key, setKey] = React.useState(""); const [value, setValue] = React.useState(""); const [cacheInput, setCacheInput] = React.useState(""); - const [cacheCursor, setCacheCursor] = React.useState(""); const [dataValue, setDataValue] = React.useState({}); const [editCache, setEditCache] = React.useState(false); const [cachedLoaded, setCachedLoaded] = React.useState(false); @@ -94,26 +122,142 @@ const CacheView = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [selectedCacheKey, setSelectedCacheKey] = useState(""); + const [totalAmount, setTotalAmount] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(50) + const [cursors, setCursors] = useState({ + 0: "", + }) + const [_, setUpdate] = useState(Math.random()) + const [selectedRows, setSelectedRows] = useState([]); // Direct category migration from ../components/Files.jsx const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [renderTextBox, setRenderTextBox] = React.useState(false); - const [fileCategories, setFileCategories] = React.useState(["default"]); + const [datastoreCategories, setDatastoreCategories] = React.useState(["default"]); const [selectedCategory, setSelectedCategory] = React.useState("default"); const [selectedFileId, setSelectedFileId] = React.useState(""); const [updateToThisCategory, setUpdateToThisCategory] = useState("") - const [showFileCategoryPopup, setShowFileCategoryPopup] = React.useState(false); - const [selectedFiles, setSelectedFiles] = useState([]); + const [workflows, setWorkflows] = useState([]); + + const [selectedFiles, setSelectedFiles] = useState([]); + const [showAutomationMenu, setShowAutomationMenu] = useState(false); + const [showSettingsMenu, setShowSettingsMenu] = useState(false); + const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false); + + const defaultAutomation = [ + { + "name": "Run workflow", + "description": "Runs a workflow with the updated value.", + "options": [{ + "key": "workflow_id", + "value": "", + }], + "icon": , + "enabled": false, + }, + { + "name": "Send message", + "description": "", + "type": "singul", + "options": [{ + "key": "app", + "value": "", + }], + "icon": , + "disabled": true, + "enabled": false, + }, + { + "name": "Enrich", + "description": "", + "type": "singul", + "options": [{ + "key": "", + "value": "", + }], + "icon": "/images/logos/singul.svg", + "enabled": false, + "disabled": true, + }, + { + "name": "Run AI Agent", + "description": "", + "options": [{ + "key": "", + "value": "", + }], + "icon": , + "enabled": false, + "disabled": true, + }, + { + "name": "Send webhook", + "description": "Sends the updated value to a specified webhook URL.", + "options": [{ + "key": "webhook_url", + "value": "", + }], + "icon": , + "enabled": false, + }, + ] + + const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation) + const [categoryConfig, setCategoryConfig] = useState(undefined) + + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const classes = useStyles(); + + + 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 workflows :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson?.success !== true) { + setWorkflows(responseJson) + } else { + toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.") + } + }) + .catch((error) => { + toast(error.toString()); + }); + } + + useEffect(() => { + setCursors({ + 0: "", + }) + setPage(0) + setSelectedRows([]) + }, [selectedCategory]) useEffect(() => { - if (orgId?.length > 0) { - listOrgCache(orgId, selectedCategory) - } - }, [orgId]) + getWorkflows() + listOrgCache(orgId, selectedCategory, 0, pageSize, page) + }, []) + const handleKeyDown = (event) => { if (event.key === 'Enter') { - fileCategories.push(event.target.value); + datastoreCategories.push(event.target.value); setSelectedCategory(event.target.value); setRenderTextBox(false); } @@ -124,9 +268,41 @@ const CacheView = memo((props) => { } } + const listOrgCache = (orgId, category, index, amount, page, keyValue) => { + setCachedLoaded(false) + if (index === undefined || index === null) { + index = 0 + } + + var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache` + + if (category !== undefined && category !== null && category !== "default" && category !== "") { + url += "?category=" + category.replaceAll(" ", "_") + } else { + url += "?category=default" + category = "default" + } + + if (amount !== undefined && amount !== null && amount > 0) { + url += "&top=" + amount + } + + if (page !== undefined && page !== null && page >= 0) { + if (cursors[page-1] !== undefined && cursors[page-1] !== null && cursors[page-1] !== "") { + url += "&cursor=" + cursors[page-1] + } + } + + if (keyValue !== undefined && keyValue !== null && keyValue !== "") { + url += "&key=" + keyValue + + setCursors({ + 0: "", + }) + setPage(0) + setSelectedRows([]) + } - const listOrgCache = (orgId, category) => { - const url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache${category !== undefined ? `?category=${category.replaceAll(" ", "_")}` : ""}` fetch(url, { method: "GET", headers: { @@ -144,26 +320,98 @@ const CacheView = memo((props) => { return response.json(); }) .then((responseJson) => { - if (responseJson.success === true) { - setListCache(responseJson.keys); - setCachedLoaded(true); + setCachedLoaded(true); + if (responseJson?.success === true) { + setListCache(responseJson.keys) - if (fileCategories.length === 1 && fileCategories[0] === "default") { + if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) { + setTotalAmount(responseJson.total_amount) + } else { + setTotalAmount(responseJson.keys.length) + } + + if (responseJson?.cursor !== undefined && responseJson?.cursor !== null && responseJson?.cursor !== "") { + cursors[page] = responseJson.cursor + } + + // Especially important during first load + if (index < 2 && (category === "default" || category === "" || category === undefined)) { + // If it exists and isn't blank/default, load it + const urlParams = new URLSearchParams(window.location.search); + const categoryParam = urlParams.get("category"); + if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") { + setSelectedCategory(categoryParam); + if (index === undefined || index === null) { + index = 0 + } + + listOrgCache(orgId, categoryParam, index+1, amount, page) + } else { + setSelectedCategory("default"); + } + } + + if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 1 && datastoreCategories[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 foundcategory = responseJson.keys[key].category + if (foundcategory !== undefined && foundcategory !== null && foundcategory !== ""){ + foundcategory = category.replaceAll(" ", "_") + + if (!newcategories.includes(foundcategory)) { + newcategories.push(foundcategory) + } } } - console.log("CATEGORIES: ", newcategories) + if (responseJson?.categories !== undefined && responseJson?.categories !== null && responseJson?.categories.length > 0) { + for (var i = 0; i < responseJson.categories.length; i++) { + const foundcategory = responseJson.categories[i].replaceAll(" ", "_") + if (foundcategory !== undefined && foundcategory !== null && foundcategory !== "" && foundcategory !== "default" && !newcategories.includes(foundcategory)) { + newcategories.push(responseJson.categories[i]); + } + } + } - setFileCategories(newcategories) + setDatastoreCategories(newcategories) } - } - if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") { - setCacheCursor(responseJson.cursor); + + if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) { + + if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") { + setCategoryConfig(responseJson.category_config) + } + + // Handle other configs here. + if (responseJson?.category_config?.automations !== undefined && responseJson?.category_config?.automations !== null && responseJson?.category_config?.automations.length > 0) { + // Find icons if they exist + for (var key in responseJson.category_config.automations) { + //if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") { + const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name) + if (foundItem) { + responseJson.category_config.automations[key].disabled = foundItem.disabled + responseJson.category_config.automations[key].icon = foundItem.icon + responseJson.category_config.automations[key].type = foundItem?.type + } else { + responseJson.category_config.automations[key].icon = + } + } + + for (var key in defaultAutomation) { + if (!responseJson.category_config.automations.some((automation) => automation.name === defaultAutomation[key].name)) { + // If the automation doesn't exist in the response, add it with default values + responseJson.category_config.automations.push(defaultAutomation[key]) + } + } + + setCategoryAutomations(responseJson.category_config.automations) + } else { + setCategoryAutomations(defaultAutomation) + } + } + } else { + toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.") } }) .catch((error) => { @@ -172,15 +420,19 @@ const CacheView = memo((props) => { }; - const deleteCache = (orgId, key) => { + const deleteEntry = (orgId, key, itemCategory, refreshList) => { const method = "POST" const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache` - const parsed = { + var parsed = { "org_id": orgId, "key": key, "category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory, } + if (itemCategory !== undefined) { + parsed["category"] = itemCategory.replaceAll(" ", "_"); + } + fetch(url, { method: method, headers: { @@ -191,12 +443,15 @@ const CacheView = memo((props) => { }) .then((response) => { if (response.status === 200) { - toast("Successfully deleted Cache"); - setTimeout(() => { - listOrgCache(orgId, selectedCategory) - }, 1000); + if (refreshList === undefined || refreshList === null || refreshList === true) { + + toast.success("Deleted datastore entry"); + setTimeout(() => { + listOrgCache(orgId, selectedCategory, 0, pageSize, page) + }, 1000); + } } else { - toast("Failed deleting Cache. Does it still exist?"); + toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`) } }) .catch((error) => { @@ -205,13 +460,25 @@ const CacheView = memo((props) => { }; const editOrgCache = (orgId) => { - const cache = { + var entry = { key: dataValue.key, value: value, - category: selectedCategory, + category: selectedCategory, } - setCacheInput([cache]); + if (dataValue?.category !== "" && dataValue?.category !== "default") { + entry.category = dataValue.category.replaceAll(" ", "_"); + + } + + if (listCache.length > 0) { + const selectedCache = listCache.find((data) => data.key === dataValue.key); + if (selectedCache?.suborg_distribution?.length > 0) { + entry.suborg_distribution = selectedCache.suborg_distribution; + } + } + + setCacheInput([entry]); fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { @@ -221,7 +488,7 @@ const CacheView = memo((props) => { Accept: "application/json", }, credentials: "include", - body: JSON.stringify(cache), + body: JSON.stringify(entry), }) .then((response) => { if (response.status !== 200) { @@ -233,8 +500,8 @@ const CacheView = memo((props) => { }) .then((responseJson) => { setAddCache(responseJson); - toast("Cache Edited Successfully!"); - listOrgCache(orgId, selectedCategory); + toast.success("Edit saved"); + listOrgCache(orgId, selectedCategory, 0, pageSize, page); setModalOpen(false); }) .catch((error) => { @@ -272,7 +539,7 @@ const CacheView = memo((props) => { .then((responseJson) => { setAddCache(responseJson); toast("New key added Successfully!"); - listOrgCache(orgId, selectedCategory); + listOrgCache(orgId, selectedCategory, 0, pageSize, page); setModalOpen(false); }) .catch((error) => { @@ -324,6 +591,19 @@ const CacheView = memo((props) => { }; + const timestamp = (timestamp) => { + if (timestamp === undefined || timestamp === null || timestamp === "") { + return null + } + + const date = new Date(timestamp * 1000); + if (date.toString() === "Invalid Date" || date.toString() === "Invalid Date NaN") { + return null + } + + return date.toISOString()?.slice(0, 19)?.replace("T", " ") + } + const modalView = ( // console.log("key:", dataValue.key), //console.log("value:",dataValue.value), @@ -354,11 +634,12 @@ const CacheView = memo((props) => { }} > - + { editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`} + -
+
Key { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -383,7 +664,7 @@ const CacheView = memo((props) => { onChange={(e) => setKey(e.target.value)} />
-
+
Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON) @@ -404,7 +685,7 @@ const CacheView = memo((props) => { style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -422,23 +703,45 @@ const CacheView = memo((props) => { value={value} onChange={(e) => setValue(e.target.value)} /> + + + {editCache ? +
+ + Created: {timestamp(dataValue?.created)} + + + Edited: {timestamp(dataValue?.edited)} + + {dataValue?.workflow_id !== "" ? + + Workflow: {dataValue.workflow_id} + + : null} + {dataValue?.category !== "" && dataValue?.category !== "default" ? + + Category: {dataValue.category} + + : null} + +
+ : null}
+ ); @@ -516,11 +820,11 @@ const CacheView = memo((props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - toast("Failed overwriting datastore"); + toast.error("Failed overwriting datastore"); } else { - toast("Successfully updated datastore!"); + toast.success("Successfully updated datastore!"); setTimeout(() => { - listOrgCache(orgId, selectedCategory); + listOrgCache(orgId, selectedCategory, 0, pageSize, page); setShowDistributionPopup(false); }, 1000); } @@ -559,9 +863,9 @@ const CacheView = memo((props) => { }} > -
+ Select sub-org to distribute Datastore key -
+
{handleSelectSubOrg(null, "none")}}>None @@ -604,7 +908,7 @@ const CacheView = memo((props) => {
+ : null} +
+ + {showOptions && ( + updatedAutomation.options.map((option, optionIndex) => { + if (option?.key === "workflow_id") { + return ( + option?.value.includes(w.id)) || []} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: theme.palette.text.primary, + borderRadius: theme.palette.borderRadius, + }, + }} + onChange={(event, newValue) => { + option.value = "" + for (var i = 0; i < newValue.length; i++) { + option.value += newValue[i].id + "," + } + + if (newValue.length > 0) { + updatedAutomation.enabled = true + } else { + updatedAutomation.enabled = false + } + + updatedAutomation.options[optionIndex] = option + setUpdatedAutomation(updatedAutomation) + setUpdated(true) + + setUpdate(Math.random()) // Force re-render + }} + + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option?.name === undefined || + option?.name === null + ) { + return "No Workflows Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " ") + + return newname + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + borderRadius: theme.palette.textFieldStyle.borderRadius, + color: theme.palette.textFieldStyle.color, + height: 35, + marginBottom: 40, + }} + renderOption={(props, data, state) => { + /* + if (data.id === option?.value) { + data = workflow; + } + */ + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } > + + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ) + }} + /> + ) + } + + return ( + { + if (e.target.value === "") { + updatedAutomation.enabled = false + } else { + updatedAutomation.enabled = true + } + + updatedAutomation.options[optionIndex].value = e.target.value; + setUpdatedAutomation(updatedAutomation) + setUpdated(true) + }} + /> + ) + }) + )} +
+ ) + } + + + const setCategorySettingsField = (field, value) => { + // Check if categoryConfig.settings is set or not. Otherwise set it. + var categoryConfig2 = categoryConfig + if (categoryConfig === undefined || categoryConfig === null) { + categoryConfig2 = {} + } + + if (categoryConfig?.settings === undefined || categoryConfig?.settings === null) { + categoryConfig2.settings = {} + } + + categoryConfig2.settings[field] = value + setCategoryConfig(categoryConfig2) + + saveAutomation( + categoryAutomations, + categoryConfig2.settings, + ) + } + + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: 'key', + headerName: 'Key', + width: 200, + filterable: true, + sortable: true, + }, + { + width: 600, + field: 'value', + filterable: true, + headerName: 'Value', + renderCell: (props) => { + const data = props.row + const validate = validateJson(data.value) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + }} + > + {validate.valid ? + { + // handleReactJsonClipboard(copy); + }} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + iconStyle={theme.palette.jsonIconStyle} + displayDataTypes={false} + onSelect={(select) => { + + // HandleJsonCopy(showResult, select, data.action.label); + console.log("SELECTED!: ", select); + }} + name={null} + /> + : + + {data.value} + + } +
+ ) + } + }, + { + field: 'actions', + headerName: 'Actions', + description: 'Actions for this key.', + width: 175, + filterable: false, + sortable: false, + + renderCell: (props) => { + const data = props.row + + return ( + + {data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined ? + + + + : ( + + + + + + + + + + )} + + + + { + // Try to make the value JSON indented + const valid = validateJson(data.value) + var newvalue = data.value + if (valid.valid) { + // JSON stringify with indentation + newvalue = JSON.stringify(valid.result, null, 2) + } + + + setEditCache(true) + setDataValue({ + "key": data.key, + "value": newvalue, + + "edited": data.edited, + "created": data.created, + "workflow_id": data.workflow_id, + "category": data.category, + }) + setValue(newvalue) + setModalOpen(true) + }} + > + + + + + + + + + { + window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank"); + }} + > + + + + + + + { + deleteEntry(orgId, data.key, data.category) + }} + > + + + + + + + + + ) + } + }, + { + field: 'distribution', + headerName: 'Distribution', + description: 'Controls whether this key is distributed to sub-organizations.', + width: 100, + filterable: false, + sortable: false, + + renderCell: (props) => { + const data = props.row + const isDistributed = data?.suborg_distribution?.length > 0 ? true : false; + + return ( +
+ {selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ? + + + + } + style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }} + /> + : + + { + setShowDistributionPopup(true) + if(data?.suborg_distribution?.length > 0){ + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedCacheKey(data.key) + }} + /> + + } + style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }} + /> + } +
+ ) + } + }, + ]; + + + const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined return ( -
+
{modalView} + + + {cacheDistributionModal} -
-
+ +
+
-

Shuffle Datastore {selectedCategory === "" || selectedCategory === "default" ? "" : `- Category '${selectedCategory}'`}

- - Datastore is a permanent key-value database for storing data that can be used cross-workflow.
You can store anything from lists of IPs to complex configurations.  + +
+ + + Shuffle Datastore + + + {userdata?.support === true ? + + + + + + : null} +
+ + + Datastore is a permanent key-value database for storing data which can be used for automation.   Learn more -
+
- - +
+ + + + : + + + + } + + {renderTextBox && { + handleKeyDown(event); + if(event.key === 'Enter' && selectedFileId.length > 0){ + setUpdateToThisCategory(event.target.value) + } + + }} + style={{ + height: 35, + width: 200, + marginTop: 0, + }} + InputProps={{ + style: { + color: theme.palette.textFieldStyle.color, height: 35, - float: "right", - position: 'relative', - top: 8 - }} - value={selectedCategory} - onChange={(event) => { - //if (selectAllChecked || listCache.length > 0) { - if (selectAllChecked || selectedFiles.length > 0) { - setUpdateToThisCategory(event.target.value) - setShowFileCategoryPopup(true) - return - } + fontSize: 16, + borderRadius: 4, + paddingTop: 0, + }, + }} + color="primary" + placeholder="Category name" + required + margin="dense" + defaultValue={""} + autoFocus + />} +
+ - setSelectedCategory(event.target.value) - if (event.target.value === "all" || event.target.value === "default") { - listOrgCache(orgId) - } else { - listOrgCache(orgId, event.target.value) - } - - // Add it to the url as a query - if (window.location.search.includes("category=")) { - const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`) - window.history.pushState({ path: newurl }, "", newurl) - } else { - window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`) - } - }} - > - {fileCategories.map((data, index) => { - return ( - - {data.replaceAll("_", " ")} - - ); - })} - - { - setShowFileCategoryPopup(false) - }} - > - File Categories - - Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category. - - - - - - - - ) : null} + + + + - - : - - + + + + + - } + - {renderTextBox && { - handleKeyDown(event); - if(event.key === 'Enter' && selectedFileId.length > 0){ - //setShowFileCategoryPopup(true) - setUpdateToThisCategory(event.target.value) - } + {showAutomationMenu || showSettingsMenu ? + { + setShowAutomationMenu(false) + setShowSettingsMenu(false) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: 500, + minHeight: 700, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + + {showSettingsMenu === true ? +
+ + + Settings for category '{selectedCategory}' + + +
+
+ + Timeout + + + You can set a timeout for the category. This will delete all keys in this category after the specified time. Timeout is in seconds and based on last EDITED time. + +
+ { + // Check if it's a number or not + var timeoutValue = 0 + if (isNaN(e.target.value) || e.target.value === "" || e.target.value === null) { + toast.info("Timeout must be a number. Setting to 0.") + } else { + timeoutValue = parseInt(e.target.value, 10) + if (timeoutValue < 60) { + toast.info("Timeout must be between 60 seconds or more. Setting to 0.") + } + + if (timeoutValue === categoryConfig?.settings?.timeout) { + return + } + } + + setCategorySettingsField("timeout", timeoutValue) + }} + /> +
+ +
+
+ + {categoryConfig?.settings?.public === true ? "" : "NOT"} Public + + + This will make the url for this category public. Metadata will be cleared, except for timestamps. Types: keys,ndjson,csv,values,json,meta + +
+
URL (when public):
{globalUrl}/api/v2/datastore/category/{selectedCategory}?top=10000&type=keys&org_id={orgId} +
+
+
+
+ + { + setCategorySettingsField("public", e.target.checked) + setUpdate(Math.random()) // Force update to re-render the component + }} + style={{marginTop: 10, }} + color="secondary" + /> + +
+
+ +
+
+ + Subscribing + + + Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet. + +
+
+
+ : +
+ + + Automation for category '{selectedCategory}' + + + + When + + + A key is edited + + + + Do + + {categoryAutomations.map((automation, index) => { + + return ( + + ) + })} +
+ } - }} - style={{ - height: 35, - width: 200, - marginTop: 0, - }} - InputProps={{ - style: { - color: "white", - height: 35, - fontSize: 16, - borderRadius: 4, - paddingTop: 0, - }, - }} - color="primary" - placeholder="Category name" - required - margin="dense" - defaultValue={""} - autoFocus - />}
+ + + : null} +
+ {isSelectedDataStore? null :} -
- - - {["Key", "Value", "Actions", "Updated", "Distribution"].map((header, index) => ( - - ))} - - {cachedLoaded === false - ? [...Array(6)].map((_, rowIndex) => ( - - {Array(5) - .fill() - .map((_, colIndex) => ( - - - - ))} - - )) - : listCache?.length === 0 ? ( - - {Array(5).fill().map((_, index) => ( - - ))} - - ): listCache?.map((data, index) => { - var category = selectedCategory - if (selectedCategory === "default") { - category = "" + { + setSelectedRows(newSelection) + }} + keepNonExistentRowsSelected={false} + getRowId={(row) => row.key} + + autoHeight={true} + sx={{ + marginTop: 1, + height: listCache.length*52+500, + width: "100%", + '.MuiTablePagination-selectLabel, .MuiTablePagination-select, .MuiTablePagination-selectIcon': { + display: 'none', + }, + marginBottom: 20, + }} + + loading={cachedLoaded === false} + pagination + paginationMode="server" + page={page} + rowCount={totalAmount} + onPageChange={(newPage, second) => { + listOrgCache(orgId, selectedCategory, 0, pageSize, newPage) + + setPage(newPage) + }} + onPageSizeChange={(newSize) => { + setPageSize(newSize); + setPage(0) + setSelectedRows([]) + + setCursors({ + 0: "", + }) + }} + + + filterMode="client" + onFilterModelChange={(model) => { + // Specific search for the key itself to find it fast across the index + if (model?.items?.length === 1) { + if (model?.items[0]?.operatorValue === "equals" && model?.items[0]?.columnField === "key") { + // Run backend search for a specific key + listOrgCache(orgId, selectedCategory, 0, pageSize, page, model?.items[0]?.value) + } + } + }} + + getRowHeight={() => { + return "auto" + }} + + hideFooterSelectedRowCount={true} + hideFooter={true} + /> + +
+
+ + {page * pageSize + 1} - {Math.min((page + 1) * pageSize, totalAmount)} of {totalAmount} + + + { + var disabled = false + if (item?.type === "page") { + if (cursors[item.page-1] === undefined) { + disabled = true + } + } + if (item?.type === "previous") { + disabled = page === 0 + } + + if (cachedLoaded === false) { + disabled = true + } + + return ( + + ) + + }} + onChange={(e, value) => { + if (value < 1) { + return } - if (data?.category === undefined && category === "") { - } else if (data?.category !== category) { - return null - } + const newPage = value-1 - var bgColor = isSelectedDataStore? "#212121":"#27292d"; - if (index % 2 === 0) { - bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023"; - } + listOrgCache(orgId, selectedCategory, 0, pageSize, newPage) - const validate = validateJson(data.value); - const isDistributed = data?.suborg_distribution?.length > 0 ? true : false; - return ( - - - { - // handleReactJsonClipboard(copy); - }} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - iconStyle={theme.palette.jsonIconStyle} - displayDataTypes={false} - onSelect={(select) => { - // HandleJsonCopy(showResult, select, data.action.label); - console.log("SELECTED!: ", select); - }} - name={"value"} - /> - : - data.value + setPage(newPage) + }} + /> + + {selectedRows.length > 0 ? +
+ + deleteEntry(orgId, selectedRows[key], foundCategory, false) + } + + setSelectedRows([]) + setTimeout(() => { + // Refresh the list + listOrgCache(orgId, selectedCategory, 0, pageSize, page) + toast.success("Deleted " + selectedRows.length + " keys from datastore") + }, 2500) + + }} + variant={"outlined"} + color="secondary" + > + + Delete {selectedRows.length} Key{ selectedRows.length > 1 ? "s" : "" } + + : null} +
+
+
); -}); +}) +//export default CacheView; export default memo(CacheView); diff --git a/frontend/src/components/ChatBot.jsx b/frontend/src/components/ChatBot.jsx new file mode 100644 index 00000000..4e491ea4 --- /dev/null +++ b/frontend/src/components/ChatBot.jsx @@ -0,0 +1,846 @@ +import React, { useState, useEffect, useContext } from "react"; + +import { useParams, useNavigate, Link } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; +import theme from '../theme.jsx'; +import Markdown from 'react-markdown' +import { isMobile } from "react-device-detect"; +import { Context } from '../context/ContextApi.jsx'; + +import AppSearch from "../components/AppSearch1.jsx"; + +import { + Divider, + ButtonGroup, + TextField, + Button, + IconButton, + Typography, + CircularProgress, + Card, + CardContent, +} from "@mui/material"; + +import { + Send as SendIcon, +} from "@mui/icons-material"; + +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import AuthenticationWindow from "../components/AuthenticationWindow.jsx"; + +const ChatBot = (props) => { + const { globalUrl } = props + const { supportEmail } = useContext(Context) + const [messages, setMessages] = useState([]) + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [appAuthentication, setAppAuthentication] = React.useState([]); + const [inputAuth, setInputAuth] = useState([]) + const [forceReauthentication, setForceReauthentication] = useState(false); + const [selectedType, setSelectedType] = useState("atomic"); + + const [appname, setAppname] = useState(""); + const [threadId, setThreadId] = useState(""); + const [runId, setRunId] = useState(""); + + const [showAppSearch, setShowAppSearch] = useState(false); + + + const waitingMsg = "Processing..." + const viewWidth = isMobile ? "92%" : 800 + + useEffect(() => { + // Check if loading and remove Waiting... from messages + const newmessages = messages + const foundmessages = messages.filter((msg) => msg.message !== waitingMsg) + if (foundmessages.length < newmessages.length) { + setMessages(foundmessages); + } + + // Wait 0.5 second + const objDiv = document.getElementById("messages-window"); + if (objDiv !== undefined && objDiv !== null) { + setTimeout(() => { + objDiv.scrollTop = objDiv.scrollHeight; + }, 250); + } + }, [messages]); + + useEffect(() => { + if (appname === undefined || appname === null || appname === "") { + return + } + + // Find the last message that was sent by us and reuse the same message content + // with added app stuff only + for (var i = messages.length-1; i >= 0; i--) { + const msg = messages[i] + if (msg.status === "sent") { + handleSubmit(undefined, msg.message) + break + } + } + }, [appname]) + + window.title = "Shuffle - New Chat" + let navigate = useNavigate(); + + // Automatic submit handler based on a lot of stuff :) + const handleSubmit = (e, inputmsg) => { + if (e !== undefined) { + e.preventDefault(); + e.stopPropagation(); + } + + setLoading(true) + setMessage(""); + + const sentId = uuidv4(); + var parsedData = { + "query": inputmsg, + "thread_id": threadId, + "run_id": runId, + } + + if (appname !== undefined && appname !== null && appname !== "") { + parsedData["app_name"] = appname + } + + if (inputAuth !== undefined && inputAuth.length > 0) { + // Forcing first auth app to be used in request + try { + parsedData["app_name"] = inputAuth[0].name + parsedData["app_id"] = inputAuth[0].id + parsedData["category"] = inputAuth[0].category + parsedData["action_name"] = inputAuth[0].action_name + } catch (e) { + } + + try { + parsedData["app_name"] = inputAuth.apps[0].name + parsedData["app_id"] = inputAuth.apps[0].id + parsedData["category"] = inputAuth.apps[0].category + parsedData["action_name"] = inputAuth.apps[0].action_name + } catch (e) { + } + } + + if (selectedType !== "default") { + if (selectedType == "workflow") { + parsedData["output_format"] = "workflow_suggestion" + } else { + parsedData["output_format"] = selectedType + } + } + + console.log("INPUT: ", parsedData) + + setInputAuth([]) + + var newmessages = messages; + newmessages.push({ + "id": sentId, + "status": "sent", + "message": inputmsg, + }) + newmessages.push({ + "id": sentId, + "status": "received", + "message": waitingMsg, + }) + + setMessages(newmessages); + + //fetch(`http://localhost:8080/api/v1/conversation`, { + fetch(`${globalUrl}/api/v1/conversation`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(parsedData), + }) + .then((res) => res.text()) + .then((resText) => { + setLoading(false) + var data = {} + + // JSON parse + try { + data = JSON.parse(resText); + } catch (e) { + console.log("Error parsing response as JSON: ", e); + + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + newmessages.push({ + "status": "received", + "message": resText, + "id": uuidv4(), + }); + + setMessages(newmessages); + + return; + } + + + if (data.run_id !== undefined && data.run_id !== null && data.run_id !== "") { + setRunId(data.run_id) + } + + if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") { + setThreadId(data.thread_id) + } + + if (data.success === undefined) { + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + newmessages.push({ + "status": "received", + "message": resText, + "id": uuidv4(), + }); + + setMessages(newmessages); + + return; + } + + // authentication for app + // app validation (choose one) + const defaultMessage = `Default output. The feature you're interacting with may not have been implemented yet. Contact ${supportEmail} with a screenshot of this and your input please.` + + var outputmessage = defaultMessage; + var status = "received"; + var action = "" + if (data.success === false) { + if (data.reason !== undefined) { + outputmessage = data.reason + } + + status = "error" + } else { + if (data.reason !== undefined) { + outputmessage = data.reason + } + } + + if (data.action !== undefined) { + //console.log("Action is defined: ", data.action); + action = data.action + + if (data.action === "app_authentication") { + // If success & app auth -> say auth success and show available labels + // If !success & app auth -> do authentication + if (data.success === true) { + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + // No action for this. + // "action": action, + var appname = "" + if (data.apps !== undefined && data.apps !== null && data.apps.length > 0) { + appname = data.apps[0].name.replaceAll("_", " ") + } + + var outputmessage = `**Please specify which ${appname} action you want to use**: \n` + if (data.available_labels !== undefined && data.available_labels !== null && data.available_labels.length > 0) { + for (var i = 0; i < data.available_labels.length; i++) { + outputmessage += "* " + data.available_labels[i] + "\n" + } + outputmessage += "* Reauthenticate ([see auth](/admin?tab=app_auth))" + } + //Some opavailable actions: " + data.apps.map((app) => app.name).join(", ") + const parsedmessage = { + "status": status, + "message": outputmessage, + "id": uuidv4(), + "category": data.category, + + "thread_id": data.thread_id, + "run_id": data.run_id, + } + newmessages.push(parsedmessage); + setMessages(newmessages); + + return + + } else { + if (data.apps !== undefined) { + setInputAuth(data.apps) + + setMessage(inputmsg); + + setForceReauthentication(true) + } + } + } else if (data.action === "select_category" || data.action === "select_app") { + console.log("[DEBUG] APP SELECTION! Should help them choose an app to use") + // Show a search field + setShowAppSearch(true) + } + } + + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + const parsedmessage = { + "status": status, + "message": outputmessage, + "id": uuidv4(), + "action": action, + "category": data.category, + + "thread_id": data.thread_id, + "run_id": data.run_id, + } + newmessages.push(parsedmessage); + setMessages(newmessages); + console.log("New message: ", parsedmessage) + }) + .catch((err) => { + setLoading(false) + console.log("Problem: ", err); + + setMessage(message); + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + + // Find the message with the sentId and change the status to error + newmessages.push({ + "status": "error", + "message": message, + "error_message": "Failed to send: "+err, + "id": sentId, + }); + setMessages(newmessages); + }); + }; + + // Used to verify if the user is logged in after auth is done + const getAppAuthentication = () => { + console.log("Continue chat from the previous stage!"); + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + console.log("Failed to get app auth!"); + return; + } + + var newauth = []; + for (let authkey in responseJson.data) { + if (responseJson.data[authkey].defined === false) { + continue; + } + + newauth.push(responseJson.data[authkey]); + } + + if (newauth.length > appAuthentication.length) { + console.log("New auth is longer than old auth. Set new auth!"); + + setForceReauthentication(false) + + // Check if last message contains "reauth" + if (messages.length > 0) { + const lastmessage = messages[messages.length-1]; + if (lastmessage.message.toLowerCase().includes("re-auth")) { + console.log("Skipping resend due to reauth") + + var newmessages = messages + newmessages.push({ + "status": "received", + "message": "Authentication done. What do you want to do?", + "id": uuidv4(), + }); + setMessages(newmessages); + + } else { + handleSubmit(undefined, message) + } + } else { + handleSubmit(undefined, message) + } + } + + setAppAuthentication(newauth) + + }) + .catch((err) => { + console.log("Error in getAppAuthentication: ", err); + }) + } + + const AuthWrapper = (props) => { + const { app } = props; + const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); + + console.log("AUTH: ", app) + + return ( +
+ {app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ? + + : + + } + + {app.authentication.type !== "oauth2" && authenticationModalOpen ? + + : null} +
+ ) + } + + var amountfinished = 0; + const showAuthentication = inputAuth.map((app, index) => { + const authexists = appAuthentication.find((auth) => auth.app.id === app.id); + if (authexists !== undefined && forceReauthentication === false) { + console.log("Auth exists: ", authexists); + amountfinished += 1 + return null + } + + return ( +
+ +
+ ) + }) + + if (amountfinished === inputAuth.length && amountfinished > 0) { + + setInputAuth([]) + setAppAuthentication([]) + } + + const showSamples = +
+ + + + How many incidents did we get last week? + + + + + + + Answer the last email from Jim about the new project, and say we're on it + + + + + + + Is the IP 1.2.3.4 blocked? If not, block it. + + + +
+ + function OuterLink(props) { + return ( + + {props.children} + + ); + } + + function Img(props) { + return {props.alt}; + } + + function CodeHandler(props) { + //console.log("Codehandler PROPS: ", props) + + const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : "" + + return ( +
+ {propvalue} +
+ ); + } + + + const markdownStyle = { + color: "rgba(255, 255, 255, 0.65)", + overflow: "hidden", + paddingBottom: 100, + margin: "auto", + maxWidth: "100%", + minWidth: "100%", + overflow: "hidden", + fontSize: isMobile ? "1.3rem" : "1.0rem", + } + + const Heading = (props) => { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: props.level === 1 ? 20 : 50 } }, + props.children + ); + const [hover, setHover] = useState(false); + + var extraInfo = ""; + return ( + { + setHover(true); + }} + > + {props.level !== 1 ? ( + + ) : null} + {element} + {/*hover ? {setHover(true)}} style={{cursor: "pointer", display: "inline", }} onClick={() => { + window.location.href += "#hello" + console.log(window.location) + //window.history.pushState('page2', 'Title', '/page2.php'); + //window.history.replaceState('page2', 'Title', '/page2.php'); + }} /> + : "" + */} + {extraInfo} + + ); + } + + const OrderedList = (props) => { + var parsedchildren = [] + for (var i = 0; i < props.children.length; i++) { + const child = props.children[i] + if (child === "\n") { + continue + } + + if (child.props !== undefined && child.props.children !== undefined) { + // Remove

from the child wrapper + var parsedchild = [] + for (var j = 0; j < child.props.children.length; j++) { + const childchild = child.props.children[j] + // print the raw childchild bytes, not string + if (childchild === "\n") { + continue + } + + // If the childchild has

around it, remove it + parsedchild.push(childchild) + + /* + // Not doing this as it breaks links + if (childchild.props !== undefined && childchild.props.children !== undefined) { + parsedchild.push(childchild.props.children) + } else { + parsedchild.push(childchild) + } + */ + } + + parsedchildren.push(parsedchild) + } + } + + return ( +

    + {parsedchildren.map((child, index) => { + return ( +
  1. +

    + {index+1}. {child} +

    +
  2. + ) + })} +
+ ) + } + + const Paragraph = (props) => { + return ( +

+ {props.children} +

+ ) + } + + const markdownComponents = { + ol: OrderedList, + ul: OrderedList, + img: Img, + code: CodeHandler, + h1: Heading, + h2: Heading, + h3: Heading, + h4: Heading, + h5: Heading, + h6: Heading, + a: OuterLink, + p: Paragraph, + } + + const chatWindow = +
+ {messages.length === 0 ? + +

Shuffle AI

+ + + + {showSamples} +
+ : null} +
+ {messages.map((message, index) => { + const float = message.status === "sent" ? "left" : "right"; + const border = message.status === "error" ? "red" : "rgba(255,255,255,0.3)" + + const hasAction = message.action !== undefined && message.action !== null && message.action !== "" + + return ( + // Make a chat bubble component +
+ { + if (!hasAction) { + return + } + + if (message.action === "login") { + navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT") + } else if (message.action === "app_authentication") { + console.log("App auth action!") + //setAuthenticationModalOpen(true) + } else { + console.log("\n\nUnknown click action: ", message.action) + } + }}> + {message.message === waitingMsg ? : null} + + + {message.message} + + + {message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" ? + + Thread: {message.thread_id} + + : null + } + + + + {message.status === "error" && message.error_message ? + + {message.error_message} + + : null} + + {(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ? +
+ +
+ : null} +
+ ) + })} +
+ {showAuthentication} + +
+
+ {messages.length === 0 ? + + + Query Type + + + {/* + + */} + + + + + : null} + + handleSubmit(e, message)} style={{bottom: 20, marginTop: 10, marginBottom: isMobile ? 0 : 10, maxWidth: viewWidth, minWidth: viewWidth, }}> + setMessage(e.target.value)} + variant="outlined" + autoFocus + InputProps={{ + endAdornment: ( + handleSubmit(e, message)} + > + + + ) + }} + /> + + + {isMobile ? null : + + {`The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`} + + } +
+
+
+ +return ( +
+ {chatWindow} +
+) +} + +export default ChatBot; diff --git a/frontend/src/components/CloudSyncTab.jsx b/frontend/src/components/CloudSyncTab.jsx index 08926570..88df2610 100644 --- a/frontend/src/components/CloudSyncTab.jsx +++ b/frontend/src/components/CloudSyncTab.jsx @@ -26,7 +26,7 @@ import { Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, } from "@mui/icons-material"; -import theme from "../theme.jsx"; +import { getTheme } from "../theme.jsx"; import { styled } from '@mui/styles'; import { Context } from "../context/ContextApi.jsx"; @@ -48,13 +48,21 @@ const CloudSyncTab = (props) => { const [, forceUpdate] = React.useState(); const itemColor = "white"; const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io"; + + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + useEffect(() => { getSettings(); }, []); + const GridItem = (props) => { const [expanded, setExpanded] = React.useState(false); const [showEdit, setShowEdit] = React.useState(false); const [newValue, setNewValue] = React.useState(-100); - const primary = props.data.primary; + var primary = props.data.primary + + const shownName = props.data.newname !== undefined && props.data.newname !== null && props.data.newname !== primary ? props.data.newname : primary + const secondary = props.data.secondary; const primaryIcon = props.data.icon; const secondaryIcon = props.data.active ? @@ -167,9 +175,9 @@ const CloudSyncTab = (props) => {
{ {primaryIcon} {isCloud && userdata.support === true ? { e.preventDefault(); @@ -475,7 +483,7 @@ const CloudSyncTab = (props) => { } else { toast("Cloud Syncronization successfully set up!"); setOrgSyncResponse( - "Successfully started syncronization. Cloud features you now have access to can be seen below." + "Successfully started syncronization. Cloud/Hybrid features are available below." ); } @@ -527,20 +535,20 @@ const CloudSyncTab = (props) => { return (
-

Cloud syncronization -

- - What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. - + + + What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. This will by default back up apps and workflows. +
{isCloud ? (
- + Currently syncronizing:{" "} {selectedOrganization.cloud_sync_active === true ? True @@ -561,28 +569,31 @@ const CloudSyncTab = (props) => { marginRight: 10, fontSize: 16, fontWeight: 400, + color: theme.palette.text.primary, fontFamily: theme.typography.fontFamily, }} > Your Api key {userSettings?.apikey === undefined || userSettings?.apikey === null || userSettings?.apikey?.length <=0 ? ( - + ):
@@ -639,14 +650,15 @@ const CloudSyncTab = (props) => { {
)} -

- Features -

- + + {isCloud ? "Cloud" : "Hybrid"} Features + + Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. - + {selectedOrganization.sync_features === undefined || selectedOrganization.sync_features === null ? + {[...Array(18)].map((_, i) => ( +
{ variant="rectangular" height={50} width={343} - sx={{ backgroundColor: '#1a1a1a', display: 'flex', borderRadius: 1 }} + sx={{ backgroundColor: theme.palette.loaderColor, display: 'flex', borderRadius: 1 }} animation="wave" />
@@ -750,6 +764,13 @@ const CloudSyncTab = (props) => { } const newkey = key.replaceAll("_", " "); + + // Rewrites to frontend names + var newname = newkey + if (newkey === "app executions") { + newname = "app runs" + } + const griditem = { primary: newkey, secondary: @@ -764,6 +785,8 @@ const CloudSyncTab = (props) => { data_collection: "None", active: item.active, icon: , + + newname: newname, }; return ( diff --git a/frontend/src/components/CollectIngestModal.jsx b/frontend/src/components/CollectIngestModal.jsx new file mode 100644 index 00000000..afba4afa --- /dev/null +++ b/frontend/src/components/CollectIngestModal.jsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect, useContext, memo } from "react"; + +import { Context } from "../context/ContextApi.jsx"; +import { getTheme } from "../theme.jsx"; +import { GetIconInfo } from "../views/Workflows2.jsx"; +import { toast } from 'react-toastify'; + +import { + Dialog, + DialogTitle, + DialogContent, + Typography, + Paper, + LinearProgress, + Grid, + Button, +} from '@mui/material'; + +import { + Rocket as RocketIcon, + FilterAlt as FilterAltIcon, +} from '@mui/icons-material'; + +const CollectIngestModal = (props) => { + const { globalUrl, open, setOpen } = props; + + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + + if (open === undefined || open === null) { + console.error("CollectIngestModal: 'open' prop is required."); + return null + } + + if (setOpen === undefined || setOpen === null) { + console.error("CollectIngestModal: 'setOpen' prop is required."); + return null + } + + const startIngestion = (appname, index) => { + console.log("APPNAME:", appname, "INDEX:", index) + + const body = { + "app_name": appname, + "label": appname, + } + + const url = `${globalUrl}/api/v2/workflows/generate` + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + credentials: "include", + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + console.log("Ingestion started successfully:", data); + toast.success(`Ingestion for ${appname} started successfully!`); + }) + .catch((error) => { + console.error("Error starting ingestion:", error); + toast.error(`Failed to start ingestion for ${appname}. Please try again.`); + }); + + } + + const IngestItem = (props) => { + const { type, index } = props + + const [hovering, setHovering] = useState(false); + const [isFinished, setIsFinished] = useState(false); + + const appname = type + const ingestedAmount = 20 + + const iconDetails = GetIconInfo({ + "app_name": appname, + "name": appname, + }) + + return ( + // setHovering(true)} + onMouseLeave={() => setHovering(false)} + > +
+ {iconDetails?.originalIcon && ( + iconDetails?.originalIcon + )} + + + + {appname} + +
+ + + {hovering ? +
+
+ : null} + + {isFinished ? +
+ + {ingestedAmount} / X + + +
+ : null} +
+ ) + } + + return ( + { + setOpen(false) + }} + > + + + + + + + Collection and Ingestion + + + + + + + + + + + + + + ) +} + +export default CollectIngestModal diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index a6ac8623..b6f0c734 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { useInterval } from "react-powerhooks"; import { toast } from 'react-toastify'; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { @@ -20,7 +20,7 @@ import { Collapse, IconButton, } from "@mui/material"; - +import { Context } from "../context/ContextApi.jsx"; import { FavoriteBorder as FavoriteBorderIcon, Error as ErrorIcon, @@ -76,6 +76,8 @@ const ConfigureWorkflow = (props) => { const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false); const [loopRunning, setLoopRunning] = useState(false) const [checkStarted, setCheckStarted] = React.useState(false); + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); useEffect(() => { if (requiredActions.length === 0) { @@ -631,7 +633,7 @@ const ConfigureWorkflow = (props) => { if (aa !== undefined) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -799,7 +801,7 @@ const ConfigureWorkflow = (props) => { >
diff --git a/frontend/src/components/Countries.jsx b/frontend/src/components/Countries.jsx index 2cb9c853..590e4caf 100644 --- a/frontend/src/components/Countries.jsx +++ b/frontend/src/components/Countries.jsx @@ -142,7 +142,6 @@ const countries = [ suggested: true, }, { code: 'GA', label: 'Gabon', phone: '241' }, - { code: 'GB', label: 'United Kingdom', phone: '44' }, { code: 'GD', label: 'Grenada', phone: '1-473' }, { code: 'GE', label: 'Georgia', phone: '995' }, { code: 'GF', label: 'French Guiana', phone: '594' }, @@ -178,7 +177,6 @@ const countries = [ { code: 'IE', label: 'Ireland', phone: '353' }, { code: 'IL', label: 'Israel', phone: '972' }, { code: 'IM', label: 'Isle of Man', phone: '44' }, - { code: 'IN', label: 'India', phone: '91' }, { code: 'IO', label: 'British Indian Ocean Territory', @@ -390,12 +388,6 @@ const countries = [ }, { code: 'UA', label: 'Ukraine', phone: '380' }, { code: 'UG', label: 'Uganda', phone: '256' }, - { - code: 'US', - label: 'United States', - phone: '1', - suggested: true, - }, { code: 'UY', label: 'Uruguay', phone: '598' }, { code: 'UZ', label: 'Uzbekistan', phone: '998' }, { diff --git a/frontend/src/components/CreatorGrid.jsx b/frontend/src/components/CreatorGrid.jsx index f3922e3e..96e9c059 100644 --- a/frontend/src/components/CreatorGrid.jsx +++ b/frontend/src/components/CreatorGrid.jsx @@ -37,7 +37,7 @@ import { AvatarGroup, } from "@mui/material" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const CreatorGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/DashboardBarchart.jsx b/frontend/src/components/DashboardBarchart.jsx index 58f20708..74970549 100644 --- a/frontend/src/components/DashboardBarchart.jsx +++ b/frontend/src/components/DashboardBarchart.jsx @@ -65,80 +65,83 @@ export const LoadStats = (globalUrl, cachekey) => { } const DashboardBarchart = (props) => { - const { timelineData, title, height, } = props; - var inputHeight = 15 - if (height !== undefined && height !== null) { - inputHeight = height - } + // this is clearly unfinished and not worth my time. + // refer to health page to see how i made it work there w/o using chartjs - const barOptions = { - plugins: { - tooltip: { - enabled: true, // Ensure tooltips are enabled - }, - }, - tooltips: { - mode: 'index', - intersect: false, - }, - legend: { - display: false - }, - layout: { - padding: { - top: 0, // Adjust the top padding as needed - bottom: -10, // Adjust the bottom padding as needed - left: 0, // Adjust the left padding as needed - right: 0, // Adjust the right padding as needed - }, - }, - scales: { - y: { - beginAtZero: false, - }, - yAxes: [{ - ticks: { - display: false - }, - beginAtZero: false, - }], - xAxes: [{ - ticks: { - display: false - }, - beginAtZero: false, - }] - }, - tooltips: { - callbacks: { - label: function (tooltipItem, data) { - const label = data.labels[tooltipItem.index] - return label.split('\n')[0] - }, - afterLabel: function (tooltipItem, data) { - const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value - return `Amount: ${amount}` - }, - title: function () { - return title === undefined ? '' : title - } - } - } - } + // const { timelineData, title, height, } = props; + // var inputHeight = 15 + // if (height !== undefined && height !== null) { + // inputHeight = height + // } - return ( - { - if (elements && elements.length > 0) { - //toast("Click event") - console.log("Clicked: ", elements) - } - }} - /> - ) + // const barOptions = { + // plugins: { + // tooltip: { + // enabled: true, // Ensure tooltips are enabled + // }, + // }, + // tooltips: { + // mode: 'index', + // intersect: false, + // }, + // legend: { + // display: false + // }, + // layout: { + // padding: { + // top: 0, // Adjust the top padding as needed + // bottom: -10, // Adjust the bottom padding as needed + // left: 0, // Adjust the left padding as needed + // right: 0, // Adjust the right padding as needed + // }, + // }, + // scales: { + // y: { + // beginAtZero: false, + // }, + // yAxes: [{ + // ticks: { + // display: false + // }, + // beginAtZero: false, + // }], + // xAxes: [{ + // ticks: { + // display: false + // }, + // beginAtZero: false, + // }] + // }, + // tooltips: { + // callbacks: { + // label: function (tooltipItem, data) { + // const label = data.labels[tooltipItem.index] + // return label.split('\n')[0] + // }, + // afterLabel: function (tooltipItem, data) { + // const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value + // return `Amount: ${amount}` + // }, + // title: function () { + // return title === undefined ? '' : title + // } + // } + // } + // } + + // return ( + // { + // if (elements && elements.length > 0) { + // //toast("Click event") + // console.log("Clicked: ", elements) + // } + // }} + // /> + // ) } export default DashboardBarchart; diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx index 7a32001c..d22beaa4 100644 --- a/frontend/src/components/DocsGrid.jsx +++ b/frontend/src/components/DocsGrid.jsx @@ -29,7 +29,7 @@ import { -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const DocsGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/EditOrgTab.jsx b/frontend/src/components/EditOrgTab.jsx index c1cd11fc..0e2b5176 100644 --- a/frontend/src/components/EditOrgTab.jsx +++ b/frontend/src/components/EditOrgTab.jsx @@ -3,6 +3,8 @@ import OrgHeaderexpanded from "../components/OrgHeaderexpandedNew.jsx"; import OrgHeader from '../components/OrgHeaderNew.jsx'; import { toast } from "react-toastify"; import CloudSyncTab from '../components/CloudSyncTab.jsx'; +import { Context } from '../context/ContextApi.jsx'; +import { getTheme } from '../theme.jsx'; import { FileCopy as FileCopyIcon, } from "@mui/icons-material"; @@ -10,6 +12,7 @@ import { Button, Tooltip, IconButton, + Typography, } from "@mui/material"; const EditOrgTab = (props) => { @@ -33,6 +36,9 @@ const EditOrgTab = (props) => { } }, []); + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const handleStatusChange = (event) => { const { value } = event.target; setSelectedStatus(value); @@ -283,23 +289,23 @@ If you're interested, please let me know a time that works for you, or set up a return ( -
-
+
+
-

Organization overview

- + Organization overview + On this page organization admins can configure organisations, and sub-orgs (MSSP).{" "} Learn more - +
{ const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, setRealtimeMarkdown, boxWidth, setBoxWidth, } = props const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove - + const {themeMode, brandColor} = useContext(Context) + const theme = getTheme(themeMode, brandColor) const [submitLoading, setSubmitLoading] = React.useState(false); const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false); @@ -187,7 +188,7 @@ const EditWorkflow = (props) => { } const newWorkflow = isEditing === true ? false : true - const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) + const priority = userdata === undefined || userdata === null || userdata.priorities === null || userdata.priorities === undefined ? null : userdata?.priorities?.find(prio => prio.type === "usecase" && prio.active === true) var upload = ""; var total_count = 0 @@ -203,26 +204,24 @@ const EditWorkflow = (props) => { setModalOpen(false); }} PaperProps={{ - style: { - color: "white", - minWidth: isMobile ? "90%" : 650, - maxWidth: isMobile ? "90%" : 650, - minHeight: 400, - paddingTop: 25, - paddingLeft: 50, + sx: { + color: theme.palette.DialogStyle.color, + minWidth: isMobile ? "90%" : "650px", + maxWidth: isMobile ? "90%" : "650px", + minHeight: "400px", //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.DialogStyle.borderRadius, + backgroundColor: themeMode === "dark" ? "black" : theme?.palette?.DialogStyle?.backgroundColor, }, }} > - +
-
+
- - {newWorkflow ? "New" : "Editing"} workflow + + {newWorkflow ? "New" : "Editing"} Workflow {newWorkflow === true ? null : @@ -248,7 +247,7 @@ const EditWorkflow = (props) => {
- Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more + Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more
@@ -288,20 +287,21 @@ const EditWorkflow = (props) => {
- +
{ @@ -395,7 +395,7 @@ const EditWorkflow = (props) => { }} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, }, }} color="primary" @@ -406,6 +406,7 @@ const EditWorkflow = (props) => { label="Name" autoFocus fullWidth + id="Enter-Workflow-Name" />
@@ -467,7 +468,7 @@ const EditWorkflow = (props) => { style={{ flex: 1, maxHeight: 120, overflow: "auto", }} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, }, }} placeholder="Tags" @@ -543,6 +544,8 @@ const EditWorkflow = (props) => { }} > } label="Test" /> + } label="Staging" /> + } label="Pre-production" /> } label="Production" /> @@ -577,12 +580,20 @@ const EditWorkflow = (props) => { Multi-Tenancy, Backups & Security - - Multi-Tenant Workflows. Make one workflow, and keep a separate, synced copy in all your tenants. Control distributed auth, runtime locations, files, datastore keys etc. (contact support@shuffler.io if you want a demo. Please try it!) + Control mechanisms for multi-tenancy, backups, and security. - {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + + Multi-Tenant Workflows + + + + Make one workflow, and keep a separate, synced copy in your other tenants. Control distributed auth, runtime locations, files, datastore keys etc. Can only distribute from parent org to child org. Need help trying it? Contact us for a demo + + + {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? @@ -600,6 +611,7 @@ const EditWorkflow = (props) => { multiple style={{ marginTop: 10, }} value={innerWorkflow.suborg_distribution === undefined || innerWorkflow.suborg_distribution === null ? ["none"] : innerWorkflow.suborg_distribution} + disabled={workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0} onChange={(e) => { var newvalue = e.target.value if (newvalue.length > 1 && newvalue[0] === "none") { @@ -1274,7 +1286,7 @@ const EditWorkflow = (props) => { {newWorkflow === true ? - + Relevant Workflows diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx index cd0a9416..6aefcf89 100644 --- a/frontend/src/components/EnvironmentTab.jsx +++ b/frontend/src/components/EnvironmentTab.jsx @@ -1,5 +1,5 @@ import React, { memo, useContext, useEffect, useState } from 'react'; -import theme from "../theme.jsx"; +import { getTheme } from "../theme.jsx"; import { Tooltip, Typography, @@ -62,6 +62,9 @@ const EnvironmentTab = memo((props) => { const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined) + const { themeMode, supportEmail, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + useEffect(() => { getEnvironments(); @@ -370,7 +373,7 @@ const EnvironmentTab = memo((props) => { }) .catch((error) => { toast( - "Failed dismissing alert. Please contact support@shuffler.io if this persists.", + `Failed dismissing alert. Please contact ${supportEmail} if this persists.`, ); }); }; @@ -512,11 +515,11 @@ const EnvironmentTab = memo((props) => { }} > - Add Location + Add Location
- Location Name + Location Name { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -539,19 +542,18 @@ const EnvironmentTab = memo((props) => { } />
- {loginInfo} {/* Assuming loginInfo is part of the relevant content */} + {loginInfo}
@@ -1420,7 +1423,7 @@ const EnvironmentTab = memo((props) => { -
+
Self-Hosted Orborus instance @@ -1443,7 +1446,7 @@ const EnvironmentTab = memo((props) => { > + label= { /> + label= Scale /> + label= k8s /> {installationTab === 2 ? - + Check our Kubernetes documentation for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected. - + : - + 1. Ensure Docker is installed and the target server can reach '{globalUrl}' - + } @@ -1501,13 +1504,19 @@ const EnvironmentTab = memo((props) => { >
{getOrborusCommand(environment)} @@ -1532,7 +1541,8 @@ const EnvironmentTab = memo((props) => {
- Configure HTTP Proxies: + Configure HTTP Proxies: { if (commandController.proxies === undefined) { @@ -1545,8 +1555,10 @@ const EnvironmentTab = memo((props) => { setUpdate(Math.random()) }} /> +
- Disable Pipelines & Data Lake: + Disable Pipelines & Data Lake: { if (commandController.pipelines === undefined) { @@ -1559,6 +1571,7 @@ const EnvironmentTab = memo((props) => { }} />
+
} diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index f825fb7f..71c3af28 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -43,7 +43,7 @@ import { import Dropzone from "../components/Dropzone.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; const Files = memo((props) => { @@ -58,6 +58,8 @@ const Files = memo((props) => { const [openEditor, setOpenEditor] = React.useState(false); const [renderTextBox, setRenderTextBox] = React.useState(false); const [loadFileModalOpen, setLoadFileModalOpen] = React.useState(false); + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); const [field1, setField1] = React.useState(""); const [field2, setField2] = React.useState(""); @@ -442,7 +444,7 @@ const Files = memo((props) => { @@ -958,9 +959,9 @@ const Files = memo((props) => { }} /> - + + @@ -1055,8 +1054,9 @@ const Files = memo((props) => { {renderTextBox ? @@ -923,19 +1106,19 @@ useEffect(() => { "& input": { padding: "8px 14px", fontSize: "14px", - color: "#C8C8C8", + color: themeMode === "dark" ? lightText : darkText, }, backgroundColor: "transparent", }} variant="outlined" InputProps={{ startAdornment: ( - + ), endAdornment: ( { }} onClick={() => { setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); }} onChange={() => { setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); }} /> ):( <> - + )} @@ -970,7 +1155,7 @@ useEffect(() => { height: "100%", overflowY: expandLeftNav ? "auto" : "hidden", scrollbarWidth: 'thin', - scrollbarColor: "#494949 transparent", + scrollbarColor: theme.palette.scrollbarColorTransparent, overflowX: "hidden", }} > @@ -983,55 +1168,73 @@ useEffect(() => { }} > - - { setOpenautomateTab((prev) => !prev); setOpenSecurityTab(false); }} style={{ - color: "#FFFFFF", + color: themeMode === "dark" ? lightText : darkText, marginLeft: 0.625, }} onMouseOver={(event)=>{ - event.currentTarget.style.backgroundColor = "#2f2f2f"; + event.currentTarget.style.backgroundColor = themeMode === "dark" ? darkHoverColor : lightHoverColor; }} onMouseOut={(event)=>{ event.currentTarget.style.backgroundColor = "transparent"; @@ -1067,8 +1270,9 @@ useEffect(() => { gap: 10, }} > - - - - - - { }} > - - + { setOpenSecurityTab((prev) => !prev); @@ -1244,10 +1440,10 @@ useEffect(() => { }} style={{ marginLeft: 0.625, - color: "#FFFFFF", + color: themeMode === "dark" ? lightText : darkText, }} onMouseOver={(event)=>{ - event.currentTarget.style.backgroundColor = "#2f2f2f"; + event.currentTarget.style.backgroundColor = themeMode === "dark" ? darkHoverColor : lightHoverColor; }} onMouseOut={(event)=>{ event.currentTarget.style.backgroundColor = "transparent"; @@ -1268,257 +1464,327 @@ useEffect(() => { )} - + + - { + setCurrentOpenTab("forms"); + localStorage.setItem("lastTabOpenByUser", "forms"); + }} + sx={{ + width: "100%", + height: 35, + color: themeMode === "dark" ? lightText : darkText, + justifyContent: "flex-start", + textTransform: "none", + backgroundColor: + currentOpenTab === "forms" || currentPath.includes("/forms") + ? themeMode === "dark" ? darkHoverColor : lightHoverColor + : "transparent", + "&:hover": { + backgroundColor: themeMode === "dark" ? darkHoverColor : lightHoverColor, + }, + cursor: "pointer", + }} + > + + • + + - - - - - - - + Forms + + + {userdata && (userdata?.support || userdata?.active_org?.role === "admin") ? ( + <> + + + - - - - - + + + - - - - - + + + + + + + + + + + + ): null} - - - - {recentworkflows?.length > 0 ? @@ -1569,7 +1833,6 @@ useEffect(() => { > { marginLeft: 5, }} > + + {userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && +
+ +
+ } { marginLeft: option.margin_left ? option.margin_left : 0, }} onMouseOver={(e) => { - e.currentTarget.style.backgroundColor = "#444444"; + e.currentTarget.style.backgroundColor = theme.palette.hoverColor; }} onMouseOut={(e) => { - e.currentTarget.style.backgroundColor = option.name === selectedOrg ? "#696969" : "transparent"; + e.currentTarget.style.backgroundColor = option.name === selectedOrg ? theme.palette.hoverColor : "transparent"; }} onClick={(e) => { if (option.id !== userdata?.active_org?.id) { @@ -1654,7 +1931,7 @@ useEffect(() => { isCloud ? ( { /> @@ -1753,7 +2030,7 @@ useEffect(() => { display: "flex", width: 48, height: 51, - border: "1px solid #494949", + border: theme.palette.defaultBorder, borderRadius: 8, justifyContent: "center", alignItems: "center", @@ -1768,6 +2045,7 @@ useEffect(() => { fontSize: "24px", opacity: expandLeftNav ? 0 : 1, transition: "opacity 0.3s ease", + color: themeMode === "dark" ? lightText : darkText, }} /> @@ -1783,112 +2061,106 @@ useEffect(() => { width: "100%", marginTop: 16, gap: 10, - justifyContent: "center", + justifyContent: "space-between", alignItems: "center", marginBottom: expandLeftNav ? 0 : 10, height: 55, borderRadius: 8, }} - onMouseOver ={(event)=>{event.currentTarget.style.backgroundColor = "#2f2f2f";setHoverOnAvatar(true)}} - onMouseLeave ={(event)=>{event.currentTarget.style.backgroundColor = "transparent";setHoverOnAvatar(false)}} + onMouseOver={(event) => { + event.currentTarget.style.backgroundColor = themeMode === "dark" ? darkHoverColor : lightHoverColor; + setHoverOnAvatar(true); + }} + onMouseLeave={(event) => { + event.currentTarget.style.backgroundColor = "transparent"; + setHoverOnAvatar(false); + }} > {expandLeftNav ? ( <> + {avatarMenu} ) : ( - <> - - + {userdata?.username?.substring(0, 1).toUpperCase()} + + )} @@ -1898,31 +2170,34 @@ useEffect(() => { }; export default LeftSideBar; - - -const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata}) => { +const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata, isDocSearchModalOpen}) => { + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode); return ( ( { setSearchBarModalOpen(false); }} PaperProps={{ - style: { - color: "white", - minWidth: 750, - height: 785, - borderRadius: 16, + sx: { + color: theme.palette.text.primary, + minWidth: "750px", + height: "785px", + borderRadius: "16px", border: "1px solid var(--Container-Stroke, #494949)", - background: "var(--Container, #000000)", + background: theme?.palette?.DialogStyle?.backgroundColor, boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", zIndex: 13000, - paddingTop: 20, + paddingTop: "20px", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, }, }} > - + diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index c29095e4..0267267d 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import ReactGA from 'react-ga4'; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import countries from "../components/Countries.jsx"; import { Box, @@ -25,12 +25,14 @@ import { CardContent, ButtonGroup, DialogContentText, + ToggleButton, + ToggleButtonGroup, } from "@mui/material"; import { useNavigate, Link } from "react-router-dom"; import { Autocomplete } from "@mui/material"; import { toast } from "react-toastify" - +import { Context } from "../context/ContextApi.jsx"; import { Cached as CachedIcon, ContentCopy as ContentCopyIcon, @@ -46,7 +48,7 @@ import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" import Billing from "./Billing.jsx"; const LicencePopup = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isLoggedIn, isMobile, selectedOrganization, isCloud } = props; + const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isScale, isLoggedIn, isMobile, selectedOrganization, isCloud, features, licensePopup = false } = props; //const alert = useAlert(); let navigate = useNavigate(); const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); @@ -72,6 +74,9 @@ const LicencePopup = (props) => { const [errorMessage, setErrorMessage] = useState("") const [highlight, setHighlight] = useState(false) + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + // Cloud const [calculatedApps, setCalculatedApps] = useState(600) const [calculatedCost, setCalculatedCost] = useState("$600") @@ -87,29 +92,235 @@ 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%" + height: "100%", + } + const userInScalePlan = userdata?.app_execution_limit > 2000 + const appRuns = (userdata?.app_execution_limit / 1000) + "K App Runs" + + // Add this function to format the limit value + const formatLimit = (limit) => { + if (limit === null || limit === undefined || limit === 0) return "Unlimited"; + if (typeof limit === "string" && limit.toLowerCase() === "unlimited") return "Unlimited"; + if (typeof limit === "number") return limit.toLocaleString(); + return limit.toString(); + }; + + // Add this function to format the feature text with proper unlimited handling + const formatFeatureText = (feature, limit) => { + if (!feature) return ""; + + // Dynamic features that use limits + const featureMapping = { + app_executions: (limit) => { + const formattedLimit = formatLimit(limit); + return `Includes ${formattedLimit} App Executions per month`; + }, + multi_env: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Environments" + : `${formattedLimit} Environment${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + multi_tenant: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Tenants" + : `${formattedLimit} Tenant${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + multi_region: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Regions" + : `${formattedLimit} Region${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + webhook: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Webhooks" + : `${formattedLimit} Webhook${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + schedules: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Schedules" + : `${formattedLimit} Schedule${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + user_input: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited User Inputs" + : `${formattedLimit} User Input${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + send_mail: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Emails per month" + : `${formattedLimit} Email${parseInt(formattedLimit) > 1 ? 's' : ''} per month`; + }, + send_sms: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited SMS per month" + : `${formattedLimit} SMS${parseInt(formattedLimit) > 1 ? 's' : ''} per month`; + }, + email_trigger: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Email Triggers" + : `${formattedLimit} Email Trigger${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + notifications: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Notifications" + : `${formattedLimit} Notification${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + workflows: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Workflows" + : `${formattedLimit} Workflow${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + autocomplete: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Autocomplete" + : `${formattedLimit} Autocomplete${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + workflow_executions: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Workflow Executions" + : `${formattedLimit} Workflow Execution${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + authentication: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Authentication" + : `${formattedLimit} Authentication${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + shuffle_gpt: (limit) => { + const formattedLimit = formatLimit(limit); + return formattedLimit === "Unlimited" + ? "Unlimited Shuffle GPT" + : `${formattedLimit} Shuffle GPT${parseInt(formattedLimit) > 1 ? 's' : ''}`; + }, + }; + + try { + // Check if we have a mapping for this feature + const formatter = featureMapping[feature]; + if (formatter) { + return formatter(limit); + } + + // Default format for unknown features + const formattedLimit = formatLimit(limit); + return `${feature}: ${formattedLimit}`; + } catch (error) { + console.warn(`Error formatting feature ${feature}:`, error); + return `${feature}: ${formatLimit(limit)}`; + } + }; + + + // Update the subscription features section billingInfo.subscription = { "active": true, - "name": "Pay as you go", - "price": typecost_single, - "currency": "USD", - "currency_text": "$", - "interval": "app run / month", - "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." + "name": appRuns, + "price": userInScalePlan ? "" : "Free", + "currency": userInScalePlan ? "" : "Free", + "currency_text": "", + "interval": "", + "description": "", + "features": userInScalePlan ? [ + // Add static features first + ...(userInScalePlan ? ["Standard Email Support"] : []), + + // Then add dynamic features from the database + ...Object.entries(features || {}) + .filter(([_, featureData]) => { + return featureData && + typeof featureData === 'object' && + featureData.active === true; + }) + .map(([featureName, featureData]) => { + try { + return formatFeatureText(featureName, featureData?.limit); + } catch (error) { + console.warn(`Error processing feature ${featureName}:`, error); + return ""; + } + }) + .filter(feature => + feature.length > 0 && + !feature.toLowerCase().includes('unlimited') // Add this filter to remove "unlimited" features + ) + ] : [ + userInScalePlan ? "Standard Email Support" : "Community Support", + userInScalePlan ? `Includes ${appRuns}. ` : `Includes ${appRuns} for free. `, + userInScalePlan ? "Multi-Tenant & Multi-Region" : "Get all 2500+ Apps and 10 Workflows", + userInScalePlan ? "All features included in the Scale plan" : "Invite up to 5 users" ], - "limit": 10000, + "limit": userInScalePlan ? userdata?.app_execution_limit : 10000, } const sendSignatureRequest = (subscription) => { @@ -143,6 +354,15 @@ const LicencePopup = (props) => { }) } + // Create a function to remove duplicates and merge features + const mergeUniqueFeatures = (existingFeatures, newFeatures) => { + // Convert arrays to Sets to remove duplicates + const uniqueFeatures = new Set([ + ...(existingFeatures || []), + ...(newFeatures || []) + ]); + return Array.from(uniqueFeatures); + }; const SubscriptionObject = (props) => { const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; @@ -151,36 +371,50 @@ 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 = userInScalePlan ? "Scale Plan" : "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", + // ] + // } + var newPaperstyle = JSON.parse(JSON.stringify(paperStyle)) if (subscription.name === "Enterprise" && subscription.active === true) { - top_text = "Current Plan" + top_text = "Enterprise Plan" - newPaperstyle.border = "1px solid #f85a3e" + // newPaperstyle.border = "1px solid #f85a3e" } 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 } @@ -250,14 +484,31 @@ const LicencePopup = (props) => { }); } + + const extraFeatures = Object.entries(features || {}) + .filter(([_, featureData]) => { + return featureData && + typeof featureData === 'object' && + featureData.active === true; + }) + .map(([featureName, featureData]) => { + return formatFeatureText(featureName, featureData?.limit); + }) + .filter(feature => + feature.length > 0 && + !feature.toLowerCase().includes('unlimited') // Add this filter to remove "unlimited" features + ) + + subscription.features = mergeUniqueFeatures(subscription.features, extraFeatures); + return ( -
- +
setHovered(true)} // onMouseLeave={() => setHovered(false)} > @@ -343,9 +594,13 @@ const LicencePopup = (props) => {
- + {subscription.active === true && !isScale && }
- {top_text === "Base Cloud Access" && userdata.has_card_available === true ? + {top_text === "Base Cloud Access" && userdata.has_card_available === true && !isScale ? { color="primary" /> : null} - + {top_text} @@ -382,7 +637,7 @@ const LicencePopup = (props) => { }} /> : null} - {isCloud && highlight === true && top_text !== "Base Cloud Access" ? + {isCloud && highlight === true && top_text !== "Starter Plan" ? { {subscription.currency_text}{subscription.price} - / {subscription.interval} + {subscription.interval.length > 0 ? `/ ${subscription.interval}` : ""}
: null} @@ -503,20 +758,17 @@ const LicencePopup = (props) => { : null} - {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 ? - `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.` } - {isCloud && (userdata.has_card_available === true || selectedOrganization?.Billing?.Email?.length > 0 )? + {/* {isCloud && (userdata.has_card_available === true || selectedOrganization?.Billing?.Email?.length > 0 )?
Billing email: {BillingEmail}
- : null} + : null} */}
{isCloud ? ( ) : null} - +
{/*
@@ -692,17 +937,42 @@ const LicencePopup = (props) => { ) } - useEffect(() => { - console.log("New variant: ", shuffleVariant) + // useEffect(() => { + // console.log("New variant: ", shuffleVariant) - if (shuffleVariant === 1) { - setCalculatedCost("$960") - setSelectedValue(8) - } 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 @@ -857,66 +1127,120 @@ const LicencePopup = (props) => { color: "white", } - 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 } - - const priceItem = window.location.origin === "https://shuffler.io" ? - shuffleVariant === 0 ? "app_executions" : "cores" - : - 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` - - console.log("Priceitem: ", priceItem, shuffleVariant) - var checkoutObject = { - lineItems: [ - { - price: priceItem, - quantity: shuffleVariant === 0 ? selectedValue / 100 : selectedValue, - }, - ], - 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: "", - }) - }) - } + + 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); + }); + }; return (
- - - {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? + + + {(selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0) && isCloud ? { 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 ( + + ) + }) + : null} + {!isCloud ? { : null} - {isCloud && + {/* {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? @@ -994,63 +1340,188 @@ const LicencePopup = (props) => { /> ) }) - : null} + : null} */} - + { + licensePopup && + ( {errorMessage.length > 0 ? Error: {errorMessage} : null} -
- - + color="primary">Recommended + + { + billingCycle === "annual" && + ( + + + 10% OFF + + + ) + } +
+
+ + {scaleValue > 300 ? "Enterprise Plan" : "Scale Plan"} + + + + + Monthly + + + Annual + + +
- {shuffleVariant === 1 ? "Scale" : "Enterprise"} - {shuffleVariant === 0 ? - "SaaS / Cloud - Per Month" - : - "Open Source + Scale License" - } + App Runs Units - { - - if (calculatedCores === "Get A Quote") { - console.log("Clicked on get a quote") - if (window.drift !== undefined) { - window.drift.api.startInteraction({ interactionId: 340785 }) - } - } - }}>{calculatedCost} - For {shuffleVariant === 1 ? `${selectedValue} CPU cores` : `${selectedValue}k App Runs`}: -
- - { - handleChange(event, newValue) +
+ + {scaleValue > 300 ? "Let's Talk" : `$${getPrice(32) * (scaleValue / 10)}`} + + 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`} +
+ + { + 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, + }, + }} + /> +
{defaultTaskIcon} - Priority Support + Standard Email Support
@@ -1069,7 +1540,7 @@ const LicencePopup = (props) => {
{defaultTaskIcon} - Help with Workflow and App development + 30 Days workflow run history
@@ -1088,18 +1559,23 @@ 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" > View all plans +
- + ) + }
) diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 00000000..07be2cb8 --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,2798 @@ +import React, { useContext, useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; + +import { + AppBar, + Box, + Toolbar, + IconButton, + Typography, + Menu, + MenuItem, + Button, + Container, + useTheme, + useMediaQuery, + Divider, + alpha, + Avatar, + Select, + Tooltip, + DialogTitle, + DialogContent, + Dialog, + Slide, + Collapse +} from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import { Close as CloseIcon, Folder as FolderIcon, Code as CodeIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material' +import SearchBox from "../components/SearchData.jsx"; +import ReactGA from "react-ga4"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardCommandKeyIcon from '@mui/icons-material/KeyboardCommandKey'; +import SearchIcon from "@mui/icons-material/Search"; +// import SearchField from "../components/Searchfield.jsx"; +import { toast } from "react-toastify"; +import AddIcon from '@mui/icons-material/Add'; +import Mousetrap from "mousetrap"; +import LicencePopup from "../components/LicencePopup.jsx"; +import { Context } from "../context/ContextApi.jsx"; +import { getTheme } from "../theme.jsx"; + +const curpath = (typeof window !== "undefined" && window.location && typeof window.location.pathname === "string") +? window.location.pathname +: ""; + +// Menu Data Structure +const menuData = { + Products: [ + { + title: "Shuffle", + description: + "The most versatile automation engine with focus on security.", + icon: "/images/icons/shuffleLogo.svg", + path: "/docs/about", + gaData: { + category: "navbar", + action: "products_click", + label: "shuffle_logo_click" + } + }, + { + title: "Singul", + description: + "Connect your favorite services with a singul line of code.", + icon: "/images/logos/singul.svg", + path: "https://singul.io", + gaData: { + category: "navbar", + action: "products_click", + label: "singul_click" + } + }, + { + title: "API Explorer", + description: + "Explore, run and automate APIs from over 2500 platforms.", + icon: "/images/icons/API.svg", + path: "/apis", + gaData: { + category: "navbar", + action: "products_click", + label: "api_explorer_click" + } + }, + ], + Services: [ + { + title: "Professional Services", + description: + "Professional Services help you solve problems at your convenience.", + icon: "/images/ProfessionalServices.svg", + path: "/professional-services", + gaData: { + category: "navbar", + action: "services_click", + label: "professional_services_click" + } + }, + { + title: "Support", + description: + "Support to help you build automations with confidence.", + icon: "/images/Support.svg", + path: "/contact?category=support", + gaData: { + category: "navbar", + action: "services_click", + label: "support_click" + } + }, + { + title: "Training", + description: + "Tailored to provide hands-on learning of Shuffle for automation mastery.", + icon: "/images/Training.svg", + path: "/training", + gaData: { + category: "navbar", + action: "services_click", + label: "training_click" + } + }, + // { + // title: "Security Consultation", + // description: + // "Automate your infrastructure with expert guidance and tailored solutions.", + // icon: "/images/SecurityConsultation.svg", + // path: "/contact?category=security_consultation", + // gaData: { + // category: "navbar", + // action: "services_click", + // label: "security_consultation_click" + // } + // }, + ], + Resources: { + columns: [ + { + title: "Platform", + items: [ + { + title: "Usecases", + icon: "/images/icons/usecases.svg", + hoverIcon: "/images/icons/usecases_hover.svg", + link: "/usecases", + gaData: { + category: "navbar", + action: "resources_click", + label: "usecases_click" + } + }, + { + title: "Documentation", + icon: "/images/icons/docs.svg", + hoverIcon: "/images/icons/docs_hover.svg", + link: "/docs/about", + gaData: { + category: "navbar", + action: "resources_click", + label: "documentation_click" + } + }, + { title: "FAQ", icon: "/images/icons/faq.svg", hoverIcon: "/images/icons/faq_hover.svg", link: "/faq", gaData: { category: "navbar", action: "resources_click", label: "faq" } }, + ], + }, + { + title: "Company", + items: [ + { + title: "About us", + icon: "/images/icons/about_us.svg", + hoverIcon: "/images/icons/about_us_hover.svg", + link: "/docs/about", + gaData: { + category: "navbar", + action: "resources_click", + label: "about_us_click" + } + }, + { + title: "Articles", + icon: "/images/icons/articles.svg", + hoverIcon: "/images/icons/articles_hover.svg", + link: "/articles/2.0_release", + gaData: { + category: "navbar", + action: "resources_click", + label: "articles_click" + } + }, + { + title: "Contact Us", + icon: "/images/icons/contact_us.svg", + hoverIcon: "/images/icons/contact_us_hover.svg", + link: "/contact?category=contact", + gaData: { + category: "navbar", + action: "resources_click", + label: "contact_us_click" + } + }, + ], + }, + { + title: "Join the community", + social: true, + platforms: [ + { name: "Discord", icon: "/images/icons/discord.svg", link: "https://discord.gg/B2CBzUm", gaData: { category: "navbar", action: "social_click", label: "discord_icon_click" } }, + { name: "GitHub", icon: "/images/icons/github.svg", link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md", gaData: { category: "navbar", action: "social_click", label: "github_icon_click" } }, + ], + followUs: [ + { name: "LinkedIn", icon: "/images/icons/linkedIn.svg", link: "https://www.linkedin.com/company/shuffleio", gaData: { category: "navbar", action: "social_click", label: "linkedin_icon_click" } }, + { name: "Twitter", icon: "/images/icons/x.svg", link: "https://twitter.com/shuffleio", gaData: { category: "navbar", action: "social_click", label: "twitter_icon_click" } }, + ], + }, + ], + }, +}; + +// Add this new component at the top level of the file, right after the imports +const LoadingSkeleton = () => { + const theme = useTheme(); + return ( + + + + + + + ); +}; + +// Add this new component for mobile menu +const MobileMenu = ({ anchorEl, handleClose, isLoggedIn, navigate, isCloud }) => { + const [openSection, setOpenSection] = useState(null); + const theme = useTheme(); + + // Add useEffect to handle body scroll + useEffect(() => { + if (Boolean(anchorEl)) { + // Disable scroll + document.body.style.overflow = 'hidden'; + } else { + // Re-enable scroll + document.body.style.overflow = 'auto'; + } + + // Cleanup function to ensure scroll is re-enabled when component unmounts + return () => { + document.body.style.overflow = 'auto'; + }; + }, [anchorEl]); + + const handleSectionClick = (section) => { + setOpenSection(openSection === section ? null : section); + }; + + const handleItemClick = (path) => { + handleClose(); + navigate(path); + }; + + const showDesktopToast = () => { + toast.info("Please open on desktop for the full experience"); + handleClose(); + }; + + return ( + + + {Object.keys(menuData).map((section) => ( + + + + + {section === 'Resources' ? ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + // onClick={() => showDesktopToast()} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + > + {platform.name} + + ))} + + + ) : ( + column.items.map((item) => ( + + )) + )} + + ))} + + ) : ( + + {menuData[section].map((item) => ( + + ))} + + )} + + + ))} + + {/* Static menu items */} + + + + {/* Action buttons */} + + + + + + + ); +}; + +const Navbar = (props) => { + const { + globalUrl, + isLoaded, + isLoggedIn, + removeCookie, + homePage, + userdata, + // isMobile, + serverside, + billingInfo, + + notifications, + } = props; + + const topbar_var = "topbar_closed10" + + const theme = useTheme(); + const {themeMode} = useContext(Context); + const currentTheme = getTheme(themeMode); + const {searchBarModalOpen, setSearchBarModalOpen, isDocSearchModalOpen, setIsDocSearchModalOpen} = useContext(Context) + const [pricingModalOpen, setPricingModalOpen] = useState(false); + const isTabletOrMobile = useMediaQuery(theme.breakpoints.down("lg")); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [anchorElNav, setAnchorElNav] = useState(null); + const [openMenu, setOpenMenu] = useState(null); + const [selectedOrganization, setSelectedOrganization] = useState({}); + const navigate = useNavigate(); + const [anchorElUser, setAnchorElUser] = useState(null); + const [anchorElOrg, setAnchorElOrg] = useState(null); + const [isOrgChanging, setIsOrgChanging] = useState(false); + const [isHovered, setIsHovered] = useState(""); + const [orgSelectOpen, setOrgSelectOpen] = useState(false); + const [showTopbar, setShowTopbar] = useState(true) // Set to true to show top bar + const isCloud = + serverside === true || typeof window === "undefined" + ? true + : window.location.host === "localhost:3002" || + window.location.host === "shuffler.io" || + window.location.host === "localhost:5002"; + + const stripeKey = typeof window === 'undefined' || window.location === undefined ? "" : window.location.origin === "https://shuffler.io" ? "pk_live_51PXYYMEJjT17t98N20qEqItyt1fLQjrnn41lPeG2PjnSlZHTDNKHuisAbW00s4KAn86nGuqB9uSVU4ds8MutbnMU00DPXpZ8ZD" : "pk_test_51PXYYMEJjT17t98NbDkojZ3DRvsFUQBs35LGMx3i436BXwEBVFKB9nCvHt0Q3M4MG3dz4mHheuWvfoYvpaL3GmsG00k1Rb2ksO" + + useEffect(() => { + // Manually setShowTopbar(true) to show topbar by default + const topbar = localStorage.getItem(topbar_var) + if (topbar === "true") { + setShowTopbar(false) + } + }, []) + + const pricingModal = + { + setPricingModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 850, + minHeight: 370, + padding: 20, + backgroundColor: "rgba(0, 0, 0, 1)", + borderRadius: theme.palette?.borderRadius, + }, + }} + > + + + Upgrade your plan + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "close_upgrade_modal", + label: "navbar_upgrade_modal", + }) + }; + setPricingModalOpen(false); + }} + style={{ + marginLeft: "auto", + position: "absolute", + top: 20, + right: 20, + }} + > + + + +
+ +
+
+ + + const modalView = ( + { + setSearchBarModalOpen(false); + }} + PaperProps={{ + sx: { + color: "white", + minWidth: "750px", + height: "785px", + borderRadius: "16px", + border: "1px solid var(--Container-Stroke, #494949)", + background: currentTheme.palette.DialogStyle.backgroundColor, + boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", + }, + '& .MuiDialogContent-root': { + backgroundColor: currentTheme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: currentTheme?.palette?.DialogStyle?.backgroundColor, + }, + }} + sx={{ + zIndex: 50005, + }} + > + + + Search for Docs, Apps, Workflows and more + + setSearchBarModalOpen(false)} + sx={{ + color: 'white', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }} + > + + + + + + + + + + + ); + + const handleOpenNavMenu = (event) => { + setAnchorElNav(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + const handleMenuOpen = (menu) => { + setOpenMenu(menu); + }; + + const handleMenuClose = () => { + setOpenMenu(null); + }; + + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleOpenOrgMenu = (event) => { + setAnchorElOrg(event.currentTarget); + }; + + const handleCloseOrgMenu = () => { + setAnchorElOrg(null); + }; + + const menuStyles = { + "& .MuiPaper-root": { + backgroundColor: "#212121", + marginTop: 1, + backdropFilter: "blur(10px)", + fontFamily: theme.typography.fontFamily, + }, + }; + + const menuItemBox = { + display: "flex", + alignItems: "flex-start", + padding: 2, + paddingTop: "20px", + paddingBottom: "20px", + paddingLeft: "20px", + gap: 2, + width: "100%", + backgroundColor: "#212121", + transition: "all 0.2s ease-in-out", + borderBottom: "1px solid #494949", + }; + + const buttonStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + textTransform: "none", + fontSize: "16px", + fontWeight: 400, + padding: "6px 12px", + "& .MuiSvgIcon-root": { + transition: "transform 0.2s ease-in-out", + }, + "&:hover": { + backgroundColor: "transparent", + color: theme.palette.primary.main, + "& .MuiSvgIcon-root": { + transform: "rotate(180deg)", + }, + }, + // Add this to hide the ripple effect's container + "& .MuiTouchRipple-root": { + display: "none", + }, + }; + + // Render menu content based on type + const renderMenuContent = (item, isCloud) => { + if (item === "Resources") { + return ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + ) : ( + + {column.items.map((item) => ( + + ))} + + )} + + ))} + + ); + } + + return menuData[item].map((menuItem, index) => ( + { + if (menuItem.title === "Singul") { + window.open(menuItem.path, '_blank'); + return; + } + if(isCloud) { + ReactGA.event(menuItem.gaData); + } + handleCloseNavMenu(); + handleMenuClose(); + if(isCloud) { + navigate(menuItem.path); + } else { + if(menuItem.path.includes("docs")){ + navigate(menuItem.path); + handleMenuClose(); + }else{ + window.open("https://shuffler.io" + menuItem.path, '_blank'); + return; + } + } + }} + sx={{ + padding: 0, + backgroundColor: "#212121", + "&:hover": { + backgroundColor: "#212121", + }, + "&:last-child": { + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + }, + "&:first-of-type": { + paddingTop: "4px", + }, + }} + > + + + + + + {menuItem.title} + + {menuItem.title === "Singul" && ( + + Beta: Coming Soon + + )} + + + {menuItem.description} + + + + + )); + }; + + const renderDesktopMenu = () => ( + + {Object.keys(menuData).map((item) => ( + + + + {/* Custom Dropdown */} + handleMenuOpen(item)} + onMouseLeave={handleMenuClose} + sx={{ + position: "absolute", + top: "130%", + left: + item === "Products" + ? 0 + : item === "Services" + ? -50 + : item === "Resources" + ? isTabletOrMobile ? -330 : -250 + : 0, + backgroundColor: "#212121", + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + boxShadow: + "0 8px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + zIndex: -20, + opacity: openMenu === item ? 1 : 0, + visibility: openMenu === item ? "visible" : "hidden", + transform: openMenu === item ? "translateY(0)" : "translateY(-8px)", + transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease", + pointerEvents: openMenu === item ? "auto" : "none", + ...(item === "Resources" && { + width: "900px", + padding: 3, + }), + }} + > + {renderMenuContent(item, isCloud)} + + + ))} + + + + + + + + + + + + ); + + const menuItemStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + "&:hover": { + backgroundColor: "rgba(255, 133, 68, 0.1)", + }, + }; + + const searchButtonStyles = { + fontFamily: theme.typography.fontFamily, + color: "rgba(255, 255, 255, 0.7)", + border: "1px solid rgba(26, 26, 26, 0.8)", + backgroundColor: "#2F2F2F", + textTransform: "none", + borderRadius: "8px", + fontSize: "14px", + padding: "6px 12px", + display: "flex", + alignItems: "center", + height: "42px", + gap: 1, + minWidth: "110px", + justifyContent: "space-between", + "&:hover": { + borderColor: "rgba(255, 255, 255, 0.1)", + }, + }; + + const searchIconButtonStyles = { + color: "rgba(255, 255, 255, 0.7)", + padding: "8px", + minWidth: "unset", + height: "42px", + "&:hover": { + backgroundColor: "#3F3F3F", + }, + }; + + // Add this shared button style + const sharedButtonStyles = { + fontFamily: theme.typography.fontFamily, + textTransform: "none", + fontSize: "16px", + fontWeight: 600, + padding: "6px 24px", + }; + + + useEffect(() => { + Mousetrap.bind(['command+k', 'ctrl+k'], () => { + setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); + return false; // Prevent the default action + }); + Mousetrap.bind(['esc'], () => { + setSearchBarModalOpen(false); + setIsDocSearchModalOpen(false); + return false; // Prevent the default action + }); + + return () => { + Mousetrap.unbind(['command+k', 'ctrl+k']); + }; + }, []); + + useEffect(() => { + if (isLoggedIn && userdata?.active_org?.id?.length > 0) { + handleGetOrg(userdata.active_org.id); + } + }, [isLoggedIn]); + + const handleGetOrg = (orgId) => { + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + console.log("Error getting org: ", responseJson); + } else { + setSelectedOrganization(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + }); + }; + + const handleClickLogout = () => { + console.log("SHOULD LOG OUT"); + + toast.info("Logging out..."); + // Don't really care about the logout + fetch(globalUrl + "/api/v1/logout", { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then(() => { + // Log out anyway + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + + removeCookie("__session", { path: "/" }); + + window.location.pathname = "/"; + + localStorage.setItem("globalUrl", "") + + // Delete userinfo from localstorage + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + }) + .catch((error) => { + console.log(error); + }); + }; + + const topbarHeight = showTopbar ? 40 : 0 + const topbar = !isCloud || !showTopbar ? null : + curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/professional-services" ? + + {/* uncommit this to show topbar for release */} + {/*
+ + // Shuffle 1.4.0 is out! Read more about  + Shuffle 2.0.0 is out now!  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_training", + label: "", + }) + + navigate("/articles/2.0_release") + + }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> + Read about it here. + + + + { + setShowTopbar(false) + + // Set storage that it's clicked + localStorage.setItem(topbar_var, "true") + }}> + + +
*/} + {/* commit below div if we need to show release stuff */} +
+ + {/* Shuffle 1.4.0 is out! Read more about  */} + New  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_training", + label: "", + }) + + navigate("/training") + + }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> + Public Training + + +  Dates Released! + + { + setShowTopbar(false) + + // Set storage that it's clicked + localStorage.setItem(topbar_var, "true") + }}> + + +
+
+ : + null + + const handleClickChangeOrg = (orgId) => { + setIsOrgChanging(true); + const data = { org_id: orgId }; + + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + toast.info("Changing organization..."); + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.removeItem("apps"); + localStorage.removeItem("workflows"); + localStorage.removeItem("userinfo"); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + if ( + responseJson.region_url !== undefined && + responseJson.region_url !== null && + responseJson.region_url.length > 0 + ) { + console.log("Region Change: ", responseJson.region_url); + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + if (responseJson["reason"] === "SSO_REDIRECT") { + toast.info("Redirecting to SSO login page as SSO is required for this organization."); + setTimeout(() => { + window.location.href = responseJson["url"]; + }, 2000); + } else { + toast("Successfully changed active organization - refreshing!"); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } else { + setIsOrgChanging(false); + if ( + responseJson.reason !== undefined && + responseJson.reason !== null && + responseJson.reason.length > 0 + ) { + toast(responseJson.reason); + } else { + toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); + } + } + }) + .catch((error) => { + console.log("error changing: ", error); + setIsOrgChanging(false); + }); + }; + + // Add these helper functions from LeftSideBar + const getRegionTag = (region_url) => { + let regiontag = "EU"; + if (region_url !== undefined && region_url !== null && region_url.length > 0) { + const regionsplit = 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"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + } else if (regiontag === "ca") { + regiontag = "CA"; + } + } + } + return regiontag; + }; + + const getRegionFlag = (region_url) => { + const regionTag = getRegionTag(region_url); + const regionMapping = { + "US": "us", + "EU": "eu", + "EU-2": "de", + "CA": "ca", + "UK": "gb" + }; + + const region = regionMapping[regionTag] || "eu"; + return `https://flagcdn.com/48x36/${region}.png`; + }; + + // Add this useEffect to prevent scroll locking + useEffect(() => { + // Remove modal classes that cause scroll locking + const removeModalClasses = () => { + document.body.style.overflow = 'auto'; + document.body.style.paddingRight = '0px'; + }; + + // Create observer to watch for class changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + if (document.body.classList.contains('MuiModal-open')) { + removeModalClasses(); + } + } + }); + }); + + // Start observing + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'], + }); + + // Cleanup + return () => { + observer.disconnect(); + removeModalClasses(); + }; + }, []); + + const handleOrgSelectClick = (event) => { + // Prevent the default Select behavior + event.preventDefault(); + if(isCloud){ + ReactGA.event({ + category: "navbar", + action: "org_dropdown_click", + label: "org_dropdown_click", + }) + } + // Toggle the select open state + setOrgSelectOpen(!orgSelectOpen); + }; + + return ( + + {topbar} + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "home_click", + label: "shuffle_logo", + }); + } + }} + > + shuffle logo + + + + {isMobile ? ( + /* Mobile Navigation Icons */ + + { + setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); + }} + > + + + { + if(Boolean(anchorElNav)) { + handleCloseNavMenu(); + }else{ + handleOpenNavMenu(e); + } + } + } + sx={{ color: "white" }} + > + {Boolean(anchorElNav) ? ( + + ) : ( + + )} + + + + ) : ( + <> + {modalView} + {pricingModal} + {renderDesktopMenu()} + {/* Right Side Buttons */} + + {!isLoaded ? ( + + ) : isLoggedIn ? ( + <> + {isOrgChanging ? ( + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); + }} + > + + + + {isCloud && (userdata.org_status === undefined || userdata.org_status === null || userdata.org_status.length === 0) ? + + : null} + + + {/* Organization Dropdown */} + {/* */} + + {/* User Avatar Menu */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "click_user_menu", + label: "open_user_dropdown" + }); + } + handleOpenUserMenu(e); + }} + sx={{ + padding: 0, + "&:hover": { + backgroundColor: "rgba(47, 47, 47, 0.5)", + }, + }} + > + + + + + + + {/* User Info Section */} + + + + + + {userdata?.username} + + + + + + + + {/* Menu Items */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_admin" + }); + } + handleCloseUserMenu(); + navigate("/admin"); + }} + onMouseEnter={() => setIsHovered("organization")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "organization" ? "#FF8544" : "white", + }, + }} + > + + + + Organization + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_settings", + }) + } + handleCloseUserMenu(); + navigate("/settings"); + }} + onMouseEnter={() => setIsHovered("settings")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "settings" ? "#FF8544" : "white", + }, + }} + > + + + + Settings + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_notifications", + }) + } + handleCloseUserMenu(); + navigate("/admin?admin_tab=notifications"); + }} + onMouseEnter={() => setIsHovered("notifications")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "notifications" ? "#FF8544" : "white", + }, + }} + > + + + + Notifications ({notifications === undefined || notifications === null ? 0 : + notifications?.filter((notification) => notification.read === false).length}) + + + + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_about", + }) + } + handleCloseUserMenu(); + navigate("/docs/about"); + }} + onMouseEnter={() => setIsHovered("about")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "about" ? "#FF8544" : "white", + }, + }} + > + + + + About + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "logout_click", + }) + } + handleCloseUserMenu(); + handleClickLogout(); + }} + onMouseEnter={() => setIsHovered("logout")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "logout" ? "#FD4C62" : "white", + }, + }} + > + + + + Logout + + + + + + + + + Version 2.0.2 + + + + + )} + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + setIsDocSearchModalOpen(false); + }} + > + + + + + + + {isCloud && + + } + + + + )} + + + )} + + + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index a3abe710..24d04adf 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -1,7 +1,8 @@ -import React, { useRef, useState, useEffect, useLayoutEffect } from "react"; +import React, { useRef, useState, useEffect, useLayoutEffect, useContext } from "react"; +import { Context } from "../context/ContextApi.jsx"; import { toast } from 'react-toastify'; import { useParams, useNavigate, Link } from "react-router-dom"; -import theme from '../theme.jsx'; +import { getTheme } from '../theme.jsx'; //import { useAlert import { v4 as uuidv4 } from "uuid"; @@ -112,6 +113,8 @@ const AuthenticationOauth2 = (props) => { authenticationType.client_secret !== null && authenticationType.client_secret.length > 0 ); + const {themeMode, brandColor} = useContext(Context); + const theme = getTheme(themeMode, brandColor); const [clientId, setClientId] = React.useState(defaultConfigSet ? authenticationType.client_id : ""); const [clientSecret, setClientSecret] = React.useState(defaultConfigSet ? authenticationType.client_secret : ""); @@ -724,7 +727,7 @@ const AuthenticationOauth2 = (props) => { style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }} src={selectedAction.large_image} /> - + One-click Login @@ -738,7 +741,7 @@ const AuthenticationOauth2 = (props) => { return (
-
+
Authenticate {selectedApp.name.replaceAll("_", " ")}
@@ -749,7 +752,7 @@ const AuthenticationOauth2 = (props) => { target="_blank" rel="norefferer" href="/docs/apps#authentication" - style={{ textDecoration: "none", color: "#f85a3e" }} + style={{ textDecoration: "none", color: theme.palette.linkColor}} > {" "} Learn more about Oauth2 with Shuffle diff --git a/frontend/src/components/OrgHeaderNew.jsx b/frontend/src/components/OrgHeaderNew.jsx index b319f3f3..1d8cbe82 100644 --- a/frontend/src/components/OrgHeaderNew.jsx +++ b/frontend/src/components/OrgHeaderNew.jsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from "react"; -import theme from "../theme.jsx"; +import React, { useEffect, useState, useContext } from "react"; +import {getTheme} from "../theme.jsx"; import { makeStyles } from "@mui/styles"; import { toast } from 'react-toastify'; - +import { Context } from "../context/ContextApi.jsx"; import { Tooltip, TextField, @@ -69,6 +69,8 @@ const OrgHeader = (props) => { selectedOrganization.description ); + const { themeMode } = useContext(Context) + const {theme} = getTheme(themeMode) const [file, setFile] = React.useState(""); const [fileBase64, setFileBase64] = React.useState( @@ -364,10 +366,10 @@ const OrgHeader = (props) => { > -
Upload Organization Image
+
Upload Organization Image
{errorText} - + { style={appIconStyle} onClick={() => { upload.click(); }} > - + @@ -399,7 +401,7 @@ const OrgHeader = (props) => { style={appIconStyle} onClick={zoomIn} > - + @@ -409,7 +411,7 @@ const OrgHeader = (props) => { style={appIconStyle} onClick={zoomOut} > - + @@ -419,7 +421,7 @@ const OrgHeader = (props) => { style={appIconStyle} onClick={rotation} > - +
@@ -427,7 +429,7 @@ const OrgHeader = (props) => {
+ + ); + + const [imageUploadError, setImageUploadError] = React.useState(""); + const [openImageModal, setOpenImageModal] = React.useState(false); + const [openLandscapeImageModal, setOpenLandscapeImageModal] = React.useState(false); + const [scale, setScale] = React.useState(1); + const [rotate, setRotation] = React.useState(0); + const [disableImageUpload, setDisableImageUpload] = React.useState(true); + const [croppedData, setCroppedData] = React.useState( + partnerData?.image_url || defaultImage + ); + const [landscapeCroppedData, setLandscapeCroppedData ] = React.useState( + partnerData?.landscape_image_url || defaultLandscapeImage + ) + + + React.useEffect(() => { + if (file.length > 0) { + setCroppedData(file); + } else if (fileBase64 !== undefined && fileBase64 !== null && fileBase64.length > 0) { + setCroppedData(fileBase64); + } else { + setCroppedData(partnerData?.image_url || defaultImage); + } + + if (landscapeFile.length > 0) { + setLandscapeCroppedData(landscapeFile); + } else if (landscapeFileBase64 !== undefined && landscapeFileBase64 !== null && landscapeFileBase64.length > 0) { + setLandscapeCroppedData(landscapeFileBase64); + } else { + setLandscapeCroppedData(partnerData?.landscape_image_url || defaultLandscapeImage); + } + + }, [partnerData, file, landscapeFile]); + + + const alternateImg = ( + { + upload.click(); + }} + /> + ); + + const zoomIn = () => { + setScale(scale + 0.1); + }; + + const zoomOut = () => { + setScale(scale - 0.1); + }; + + const rotation = () => { + setRotation(rotate + 10); + }; + + const onPositionChange = () => { + setDisableImageUpload(false); + }; + + const onCancelSaveAppIcon = () => { + setOpenImageModal(false); + setOpenLandscapeImageModal(false); + setImageUploadError(""); + }; + + let editor; + let landscapeEditor; + const setEditorRef = (imgEditor) => { + editor = imgEditor; + }; + const setLandscapeEditorRef = (imgEditor) => { + landscapeEditor = imgEditor; + }; + + + const onSaveAppIcon = () => { + const canvas = editor.getImageScaledToCanvas(); + const newImageData = canvas.toDataURL(); + setCroppedData(newImageData); // Update croppedData with the new image data + setOpenImageModal(false); + setDisableImageUpload(true); + setPartnerData({ + ...partnerData, + image_url: newImageData, + }) + }; + + const onSaveLandscapeImage = () => { + const canvas = landscapeEditor.getImageScaledToCanvas(); + const newImageData = canvas.toDataURL(); + setLandscapeCroppedData(newImageData); + setOpenLandscapeImageModal(false); + setDisableImageUpload(true); + setPartnerData({ + ...partnerData, + landscape_image_url: newImageData, + }) + } + + const imageInfo = ( + + ); + + const landScapeImageInfo = ( + + ); + + const errorText = imageUploadError.length > 0 ? ( +
Error: {imageUploadError}
+ ) : null; + + + const imageUploadModalView = openImageModal && !isDisabled ? ( + + + +
Upload Partner Image
+
+ {errorText} + + setRotation(0)} + /> + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ ) : null; + + const imageUploadModalViewLandscape = openLandscapeImageModal && !isDisabled ? ( + + + +
Upload Partner Landscape Image
+
+ {errorText} + + setRotation(0)} + /> + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ ) : null; + + + + if (loadingPartnerData) { + return ( +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ ); + } + + return ( +
+
+ +
{ + setOpenImageModal(true); + }} + > + (upload = ref)} + onChange={(e) => { + const reader = new FileReader(); + reader.onload = (event) => { + setCroppedData(event.target.result); + }; + reader.readAsDataURL(e.target.files[0]); + }} + /> + {imageInfo} +
+ {imageUploadModalView} +
+
+
+ +
+
+ +
+
+
+
+ +
{ + setOpenLandscapeImageModal(true); + }} + > + (landScapeUpload = ref)} + onChange={(e) => { + const reader = new FileReader(); + reader.onload = (event) => { + setLandscapeCroppedData(event.target.result); + }; + reader.readAsDataURL(e.target.files[0]); + }} + /> + {landScapeImageInfo} +
+ {imageUploadModalViewLandscape} +
+
+
+ +
+
+ +
+
+
+
+ ); +}; + +export default PartnerHeader; diff --git a/frontend/src/components/PartnerSettings.jsx b/frontend/src/components/PartnerSettings.jsx new file mode 100644 index 00000000..a979c7d7 --- /dev/null +++ b/frontend/src/components/PartnerSettings.jsx @@ -0,0 +1,391 @@ +import React, { useEffect, useState, useContext } from 'react'; +import { toast } from "react-toastify"; +import { Context } from '../context/ContextApi.jsx'; +import { getTheme } from '../theme.jsx'; +import { + Button, + Typography, + Box, + IconButton, + Tooltip, +} from "@mui/material"; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { useNavigate } from 'react-router'; +import PartnerHeader from '../components/PartnerHeader.jsx'; +import PartnerDetails from '../components/PartnerDetails.jsx'; + +const PartnerSettings = (props) => { + const { + isCloud, + userdata, + globalUrl, + serverside, + loadingPartnerData, + selectedOrganization, + setSelectedOrganization, + handleGetOrg, + partnerData, + setPartnerData, + } = props; + + const [isPartner, setIsPartner] = React.useState(isCloud ? userdata?.active_org?.is_partner ? true : false : false); + const [partnerTypes, setPartnerTypes] = React.useState({}); + const [isPublishing, setIsPublishing] = React.useState(false); + const [isToggling, setIsToggling] = React.useState(false); + + useEffect(() => { + setIsPartner(isCloud ? userdata?.active_org?.is_partner ? true : false : false); + if(userdata?.support){ + setIsPartner(true); + } + }, [userdata, isCloud]); + // Partner Types handling : Getting from org status + useEffect(() => { + const partnerTypes = {}; + userdata?.org_status.forEach(status => { + if (status.includes("_partner")) { + partnerTypes[status] = true; + } + }); + setPartnerTypes(partnerTypes); + }, [selectedOrganization]); + // Partner Type Colors + const partnerTypeColors = { + "tech_partner": "#ff8544", + "distribution_partner": "#2BC07E", + "service_partner": "#a99cf9", + "integration_partner": "#fb47a0" + } + + const handleSendUpdateRequest = () => { + toast("Your request has been sent to the support team. They will review your request and get back to you as soon as possible.") + } + + // Open partner page in new tab + const handleOpenPartnerPage = () => { + if (partnerData?.id) { + if(partnerData?.public){ + window.open(`${window.location.origin}/partners/${partnerData.name.toLowerCase().replaceAll(" ", "_")}`) + }else{ + window.open(`${window.location.origin}/partners/${partnerData?.id}`, "_blank"); + } + } else { + toast.error("Partner ID not found"); + } + } + + // Update Partner Details + const handleUpdatePartnerDetails = () => { + // Validate required fields before publishing + const requiredStringFields = [ + { field: partnerData?.name, name: "Partner Name" }, + { field: partnerData?.description, name: "Description" }, + { field: selectedOrganization?.id, name: "Organization Id" }, + { field: partnerData?.image_url, name: "Logo Image" }, + { field: partnerData?.landscape_image_url, name: "Landscape Image" }, + { field: partnerData?.website_url, name: "Website URL" }, + { field: partnerData?.article_url, name: "Article URL" }, + { field: partnerData?.country, name: "Country" }, + { field: partnerData?.region, name: "Region" }, + ]; + + // Required array fields (multi-select dropdowns) + const requiredArrayFields = [ + { field: partnerData?.solutions, name: "Solutions" }, + ]; + + // Check if any required string fields are empty + const emptyStringFields = requiredStringFields.filter(item => + !item.field || item.field.trim() === "" + ); + + // Check if any required array fields are empty + const emptyArrayFields = requiredArrayFields.filter(item => + !item.field || !Array.isArray(item.field) || item.field.length === 0 + ); + + // Combine all empty fields + const allEmptyFields = [...emptyStringFields, ...emptyArrayFields]; + + // If there are empty required fields, show error and return + if (allEmptyFields.length > 0) { + const missingFields = allEmptyFields.map(item => item.name).join(", "); + toast.error(`Please fill in all required fields: ${missingFields}`); + return; + } + + // Check if at least one partner type is selected + if (Object.keys(partnerTypes).length === 0) { + toast.error("There should be at least one partner type"); + return; + } + + setIsPublishing(true); + const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id; + const data = { + id: partnerData.id?.trim() || null, + name: partnerData.name?.trim(), + org_id: selectedOrganization?.id?.trim(), + description: partnerData.description?.trim(), + website_url: partnerData.website_url?.trim(), + article_url: partnerData.article_url?.trim(), + partner_type: partnerTypes, + expertise: partnerData?.expertise || [], + services: partnerData?.services || [], + solutions: partnerData?.solutions || [], + country: partnerData?.country || "", + region: partnerData?.region || "", + image_url: partnerData?.image_url?.trim(), + landscape_image_url: partnerData?.landscape_image_url?.trim(), + public: partnerData?.public + } + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + setIsPublishing(false); + if (response.status !== 200) { + toast.error("Failed to publish partner"); + } + return response.json(); + }) + .then((responseJson) => { + toast.success("Partner details successfully updated"); + }) + .catch((error) => { + setIsPublishing(false); + toast.error("Failed to update partner details: " + error?.message); + }) + } + + // Toggle Partner Publish Status + const handleTogglePublishStatus = () => { + setIsToggling(true); + const newPublishStatus = !partnerData?.public; + const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id; + + const data = { + id: partnerData.id?.trim() || null, + name: partnerData.name?.trim(), + org_id: selectedOrganization?.id?.trim(), + description: partnerData.description?.trim(), + website_url: partnerData.website_url?.trim(), + article_url: partnerData.article_url?.trim(), + partner_type: partnerTypes, + usecases: partnerData?.usecases || [], + expertise: partnerData?.expertise || [], + services: partnerData?.services || [], + solutions: partnerData?.solutions || [], + country: partnerData?.country || "", + region: partnerData?.region || "", + image_url: partnerData?.image_url?.trim(), + landscape_image_url: partnerData?.landscape_image_url?.trim(), + public: newPublishStatus + } + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + setIsToggling(false); + if (response.status !== 200) { + toast.error("Failed to update publish status"); + } + return response.json(); + }) + .then((responseJson) => { + // Update local state + setPartnerData(prev => ({ + ...prev, + public: newPublishStatus + })); + toast.success(`Partner ${newPublishStatus ? 'published' : 'unpublished'} successfully`); + }) + .catch((error) => { + setIsToggling(false); + toast.error("Failed to update publish status: " + error?.message); + }) + } + + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + const navigate = useNavigate(); + + return ( +
+
+
+
+
+ + Configuration + {Object?.entries(partnerTypes)?.map(([key, value]) => ( + + {key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())} + + ))} + + + + Set up and manage partner details and information to be displayed on the Partners page. + + + {isPartner && ( + <> + {/* View Partner Page Button */} + + + + + + + {/* Update Details Button */} + + {/* Toggle Publish/Unpublish Button */} + + + )} + + {!isPartner && ( + + )} + + +
+
+
+ + +
+
+ ) +} + +export default PartnerSettings; diff --git a/frontend/src/components/PartnerTab.jsx b/frontend/src/components/PartnerTab.jsx new file mode 100644 index 00000000..10c21f06 --- /dev/null +++ b/frontend/src/components/PartnerTab.jsx @@ -0,0 +1,245 @@ +import React, { useEffect, useState, useCallback, useContext } from 'react'; +import { useNavigate, useLocation } from "react-router-dom"; +import Branding from "../components/Branding.jsx"; +import { ToastContainer, toast } from "react-toastify"; +import { Button } from '@mui/material'; +import { getTheme } from '../theme.jsx'; +import { Context } from '../context/ContextApi.jsx'; +import PartnerSettings from '../components/PartnerSettings.jsx'; +import PartnersUsecasesTab from '../components/PartnersUsecasesTab.jsx'; +import PartnersApps from '../components/PartnerApps.jsx'; +import PartnerArticles from '../components/PartnersArticles.jsx'; +const PartnerTab = (props) => { + const location = useLocation(); + const navigate = useNavigate(); + const { + userdata, + globalUrl, + serverside, + isCloud, + setSelectedOrganization, + selectedOrganization, handleGetOrg, + handleStatusChange, + isLoaded, + removeCookie + } = props; + + + const [selectedTab, setSelectedTab] = useState('partner_settings'); + const [loadingPartnerData, setLoadingPartnerData] = useState(false); + const [curIndex, setCurIndex] = React.useState(0); + const [partnerData, setPartnerData] = React.useState({ + name: "", + description: "", + image_url: "", + landscape_image_url: "", + website_url: "", + article_url: "", + expertise: [], + usecases: [], + services: [], + solutions: [], + country: "", + region: "", + partner_type: {}, + }); + + const tabsOnPartnerTab = [ 'Partner Settings', 'Usecases', 'Apps', 'AI Agents', 'Articles', 'Branding']; + + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + + useEffect(() => { + if(!isCloud || !userdata?.active_org?.is_partner) { + // If the user is not a partner or if it's not a cloud environment do not make api call :) + return; + } + setLoadingPartnerData(true); + const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed to get partner data") + } + return response.json(); + }) + .then((responseJson) => { + if(responseJson.success) { + setPartnerData(responseJson?.partner); + console.log("responseJson", responseJson) + setLoadingPartnerData(false); + }else{ + setLoadingPartnerData(false); + toast(responseJson?.reason) + } + }) + .catch((error) => { + setLoadingPartnerData(false); + }) + }, [globalUrl]); + + // Used to auto select the tab based on the url : partner_tab + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams.get('partner_tab'); + if (tabName) { + const decodedTabName = decodeURIComponent(tabName); + setSelectedTab(decodedTabName); + if (decodedTabName === 'partner_settings') { + setCurIndex(0); + } else if(decodedTabName === 'usecases'){ + setCurIndex(1) + }else if (decodedTabName === 'apps'){ + setCurIndex(2); + } else if (decodedTabName === 'aiagents') { + setCurIndex(3); + } else if (decodedTabName === 'articles') { + setCurIndex(4); + } else if (decodedTabName === 'branding') { + setCurIndex(5); + } + } + }, [location.search]); + + // Tab click on partner tab + const handleTabClick = (tabName) => { + const formattedTabName = tabName.toLowerCase().replace(/[\s&]+/g, ''); + const encodedTabName = encodeURIComponent(formattedTabName); + setSelectedTab(formattedTabName); + document.title = `Shuffle - partner - ${formattedTabName}`; + navigate(`?partner_tab=${encodedTabName}`); + }; + + // Rendering the content based on the selected tab + const renderContent = () => { + switch (selectedTab) { + case 'partner_settings': + return ; + case 'usecases': + return ; + case `apps`: + return ; + case 'articles' : + return ; + case `ai_agents`: + return ; + case 'branding': + return ; + default: + return ; + } + }; + + const isTabDisabled = (tabName) => { + // If user is a support user, enable all tabs + if (userdata?.support) { + return false; + } + + // For non-support users, apply the following restrictions: + + // For onPrem only partner settings and branding are enabled + if ( + !isCloud && + (tabName === "Usecases" || + tabName === "Apps" || + tabName === "AI Agents" || + tabName === "Articles") + ) { + return true; + } + + // Disable Apps, Articles, and AI Agents for all non-support users + if ( + tabName === "Apps" || + tabName === "Articles" || + tabName === "AI Agents" + ) { + return true; + } + + // Disable Usecases tab for cloud users if not a partner or is in a sub-org + if (isCloud && tabName === "Usecases") { + if ( + !userdata?.active_org?.is_partner || + userdata?.active_org?.is_sub_org + ) { + return true; + } + } + + // Disable Branding tab if not an integration partner + const isIntegrationPartner = + userdata && + userdata?.org_status?.includes("integration_partner") && + !userdata?.org_status?.includes("sub_org"); + if (tabName === "Branding" && !isIntegrationPartner) { + return true; + } + + // Enable the tab by default + return false; + } + + return ( +
+
+ {tabsOnPartnerTab?.map((tabName, index) => ( +
+ +
+ ))} +
+
+ {renderContent()} +
+
+ ); +}; + +export default PartnerTab; diff --git a/frontend/src/components/PartnersArticles.jsx b/frontend/src/components/PartnersArticles.jsx new file mode 100644 index 00000000..80653231 --- /dev/null +++ b/frontend/src/components/PartnersArticles.jsx @@ -0,0 +1,21 @@ +import { Box, Typography } from '@mui/material' +import React from 'react' + +const PartnersArticles = () => { + return ( + + Partner Articles + + ) +} + +export default PartnersArticles diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx new file mode 100644 index 00000000..87e34b52 --- /dev/null +++ b/frontend/src/components/PartnersUsecasesTab.jsx @@ -0,0 +1,1761 @@ +import { + Box, + Tooltip, + Typography, + Switch, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + IconButton, + Menu, + ListItemIcon, + ListItemText, + Checkbox, + CircularProgress, + Skeleton, +} from "@mui/material"; +import React, { useContext, useEffect, useState } from "react"; +import { getTheme } from "../theme.jsx"; +import { Context } from "../context/ContextApi.jsx"; +import AddIcon from "@mui/icons-material/Add"; +import CloseIcon from "@mui/icons-material/Close"; +import { toast } from "react-toastify"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import EditIcon from "@mui/icons-material/Edit"; +import StarIcon from "@mui/icons-material/Star"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import { Link, useNavigate } from "react-router-dom"; + +// Helper function to get the correct image path based on app category +export const getCategoryImagePath = (category) => { + if (!category) return "/images/appCategories/other.svg"; + + const lowerCategory = category.toLowerCase(); + + if (lowerCategory.includes("communication")) { + return "/images/appCategories/cases.svg"; + } else if (lowerCategory.includes("cases")) { + return "/images/appCategories/cases.svg"; + } else if (lowerCategory.includes("network")) { + return "/images/appCategories/network.svg"; + } else if (lowerCategory.includes("siem")) { + return "/images/appCategories/siem.svg"; + } else if (lowerCategory.includes("edr") || lowerCategory.includes("eradication")) { + return "/images/appCategories/edr.svg"; + } else if (lowerCategory.includes("iam")) { + return "/images/appCategories/iam.svg"; + } else if (lowerCategory.includes("assets")) { + return "/images/appCategories/assets.svg"; + } else if (lowerCategory.includes("intel")) { + return "/images/appCategories/intel.svg"; + } else if (lowerCategory.includes("email")) { + return "/images/appCategories/email.svg"; + } else { + return "/images/appCategories/other.svg"; + } +}; + +// Skeleton component for loading state +const UsecaseCardSkeleton = () => { + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + + return ( + + +
+ {/* Source App Icon Skeleton */} + + + {/* Destination App Icon Skeleton */} + +
+
+ + +
+
+ +
+ ); +}; + +const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + const navigate = useNavigate(); + + const handleClick = (event) => { + event.stopPropagation(); + event.preventDefault(); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleEdit = () => { + console.log("Edit usecase:", usecase.id); + handleClose(); + }; + + const handleStar = () => { + console.log("Star usecase:", usecase.id); + handleClose(); + }; + + const handleCardClick = (e) => { + // Navigate to usecase detail page + navigate(`/usecases/${usecase.id}`); + }; + + return ( + + +
+ {/* Source App Icon */} + + Source + + + {/* Destination App Icon */} + + Destination + +
+
+ { + e.stopPropagation(); + }} + onChange={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleToggle(usecase.id); + }} + size="medium" + sx={{ + "& .MuiSwitch-switchBase.Mui-checked": { + color: "#4CAF50", + }, + "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { + backgroundColor: "#4CAF50", + }, + }} + /> + { + e.stopPropagation(); + handleClick(e); + }} + size="small" + sx={{ + color: theme.palette.text.primary, + zIndex: 20, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)", + }, + }} + > + + + e.stopPropagation()} + PaperProps={{ + sx: { + backgroundColor: theme.palette.DialogStyle.backgroundColor, + border: theme.palette.defaultBorder, + borderRadius: "8px", + boxShadow: theme.palette.DialogStyle.boxShadow, + "& .MuiMenuItem-root": { + fontSize: "14px", + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)", + }, + }, + }, + }} + transformOrigin={{ horizontal: "right", vertical: "top" }} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + > + { + e.stopPropagation(); + handleOpenDialog(usecase); + setAnchorEl(null); + }} + > + + + + Edit Usecase + + { + e.stopPropagation(); + handleDeleteUsecase(usecase.id); + }}> + + + + Delete Usecase + + +
+
+ + {usecase.name} + +
+ ); +}; + +const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => { + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + + // Dialog state + const [openDialog, setOpenDialog] = useState(false); + const [publicWorkflows, setPublicWorkflows] = useState([]); + const [usecaseData, setUsecaseData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isWorkflowLoading, setIsWorkflowLoading] = useState(false); + const [formData, setFormData] = useState({ + id: "", + mainContent: { + title: "", + description: "", + categories: [], + publicWorkflowId: "", + sourceAppType: "", + destinationAppType: "", + }, + navigation: { + items: [ + { + name: "About Usecase", + content: [""], + }, + ], + }, + public: false, + }); + + // App categories for dropdowns + const appCategories = [ + { value: "communication", label: "Communication" }, + { value: "cases", label: "Cases" }, + { value: "network", label: "Network" }, + { value: "siem", label: "SIEM" }, + { value: "edr", label: "EDR" }, + { value: "eradication", label: "Eradication" }, + { value: "iam", label: "IAM" }, + { value: "assets", label: "Assets" }, + { value: "intel", label: "Intel" }, + { value: "email", label: "Email" }, + { value: "other", label: "Other" } + ]; + + // Sample workflow options + const workflowOptions = [ + "Email Analysis Workflow", + "Incident Response Workflow", + "Alert Triage Workflow", + "Threat Hunting Workflow", + "Vulnerability Management", + ]; + + useEffect(() => { + // Set loading state to true when fetching starts + setIsLoading(true); + if(!isCloud || !userdata?.active_org?.is_partner) { + // If the user is not a partner or if it's not a cloud environment do not make api call :) + return; + } + // Load usecase data from API + fetch(`${globalUrl}/api/v1/partners/${partnerData?.id}/usecases`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for PARTNER USECASES :O!"); + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + const usecases = responseJson.usecases.map((usecase) => ({ + id: usecase.id, + sourceAppType: usecase.mainContent?.sourceAppType || "", + destinationAppType: usecase.mainContent?.destinationAppType || "", + srcImg: getCategoryImagePath(usecase.mainContent?.sourceAppType), + dstImg: getCategoryImagePath(usecase.mainContent?.destinationAppType), + publicWorkflowId: usecase.mainContent?.publicWorkflowId || "", + name: usecase.mainContent?.title, + description: usecase.mainContent?.description, + navigation: usecase.navigation || { items: [] }, + categories: usecase.mainContent?.categories || [], + public: usecase?.public || false, + })); + if (usecases.length > 0) { + setUsecaseData(usecases); + } else { + setUsecaseData([]); + } + } + }) + .catch((error) => { + console.log(error); + }) + .finally(() => { + // Set loading state to false when fetching completes (success or error) + setIsLoading(false); + }); + }, [partnerData?.id]); + + // Sample categories + const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"]; + + const getUserProfileWorkflows = (orgId) => { + setIsWorkflowLoading(true); + fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setPublicWorkflows(responseJson || []); + setIsWorkflowLoading(false); + }else { + toast.info(responseJson.message || "No public workflows found for this organization."); + setPublicWorkflows([]); + } + }) + .catch((error) => { + console.log(error); + setIsWorkflowLoading(false); + }); + } + + useEffect(() => { + const orgId = userdata?.active_org?.id; + if(!isCloud || !userdata?.active_org?.is_partner) { + // If the user is not a partner or if it's not a cloud environment do not make api call :) + return; + } + getUserProfileWorkflows(orgId); + }, []); + + + const handleUpdateUsecase = async (usecaseId, updatedData) => { + try { + // Transform the frontend data structure to match the backend expected structure + const backendUsecaseData = { + id: updatedData.id, + companyInfo: { + id: partnerData?.id.trim(), + name: partnerData?.name.trim(), + }, + mainContent: { + title: updatedData.name.trim(), + description: updatedData.description.trim(), + categories: updatedData.categories, + publicWorkflowId: updatedData.publicWorkflowId.trim(), + sourceAppType: updatedData.sourceAppType.trim(), + destinationAppType: updatedData.destinationAppType.trim(), + }, + navigation: updatedData.navigation, + public: updatedData.public, + edited: Date.now(), + }; + + + // return + const response = await fetch(`${globalUrl}/api/v1/usecases/${usecaseId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(backendUsecaseData), + }); + + if (response.status !== 200) { + console.error("Failed to update usecase"); + toast.error("Failed to update usecase"); + return false; + } + + const responseData = await response.json(); + toast.success("Usecase published status updated successfully"); + return true; + } catch (error) { + console.error("Error updating usecase:", error); + toast.error("Error updating usecase"); + return false; + } + }; + + const handleDeleteUsecase = async (usecaseId) => { + try { + const response = await fetch( + `${globalUrl}/api/v1/usecases/${usecaseId}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + } + ); + if (response.status !== 200) { + console.error("Failed to delete usecase"); + toast.error("Failed to delete usecase"); + return false; + } + const responseData = await response.json(); + toast.success("Usecase deleted successfully"); + // Remove the deleted usecase from the state + setUsecaseData((prevData) => + prevData?.filter((usecase) => usecase.id !== usecaseId) + ); + // Remove the usecase ID from partnerData + setPartnerData((prevData) => ({ + ...prevData, + usecases: prevData.usecases?.filter((id) => id !== usecaseId), + })); + return true; + } catch (error) { + console.error("Error deleting usecase:", error); + toast.error("Error deleting usecase"); + return false; + } + }; + + const handleToggle = async (currentId) => { + // Find the current usecase + const currentUsecase = usecaseData.find(usecase => usecase.id === currentId); + if (!currentUsecase) return; + + // Optimistically update the UI + setUsecaseData((prevData) => + prevData.map((usecase) => + usecase.id === currentId + ? { ...usecase, public: !usecase.public } + : usecase + ) + ); + + // Prepare the updated data + const updatedPublishedStatus = !currentUsecase.public; + + // Create a copy of the usecase with updated published status + const updatedUsecase = { + ...currentUsecase, + public: updatedPublishedStatus + }; + toast.info(`Updating usecase ${updatedUsecase.name}...`,{ + autoClose: 1000, + }) + // Send the update to the server + const success = await handleUpdateUsecase(currentId, updatedUsecase); + + // If the update failed, revert the UI change + if (!success) { + setUsecaseData((prevData) => + prevData.map((usecase) => + usecase.id === currentId + ? { ...usecase, public: currentUsecase.public } + : usecase + ) + ); + } + }; + + const handleOpenDialog = (usecase) => { + setFormData({ + id: usecase?.id || undefined, + companyInfo: { + id: usecase.companyInfo?.id || "", + name: usecase.companyInfo?.name || "", + }, + mainContent: { + title: usecase?.name, + description: usecase?.description, + categories: usecase?.categories || [], + publicWorkflowId: usecase?.publicWorkflowId || "", + sourceAppType: usecase?.sourceAppType || "", + destinationAppType: usecase?.destinationAppType || "", + }, + navigation: usecase?.navigation || { + items: [ + { + name: "About Usecase", + content: [""], + }, + ], + }, + public: usecase?.public || false, + }); + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + setIsSubmitting(false); // Reset loading state when closing dialog + setFormData({ + id: "", + mainContent: { + title: "", + description: "", + categories: [], + publicWorkflowId: "", + sourceAppType: "", + destinationAppType: "", + }, + navigation: { + items: [ + { + name: "About Usecase", + content: [""], + }, + ], + }, + public: false, + }); + }; + + const handleSubmit = () => { + // Validate required fields + const validationErrors = []; + + // Check for public workflow selection + if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") { + validationErrors.push("Please select a public workflow"); + } + + if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") { + validationErrors.push("Source app type is required"); + } + + if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") { + validationErrors.push("Destination app type is required"); + } + + // Validate that source and destination are different + if (formData.mainContent.sourceAppType && formData.mainContent.destinationAppType && + formData.mainContent.sourceAppType.trim() === formData.mainContent.destinationAppType.trim()) { + validationErrors.push("Source and destination app types must be different"); + } + + // Check main content fields + if (!formData.mainContent.title || formData.mainContent.title.trim() === "") { + validationErrors.push("Title is required"); + } else if (formData.mainContent.title.length < 5) { + validationErrors.push("Title must be not be less than 5 characters"); + } + + if (!formData.mainContent.description || formData.mainContent.description.trim() === "") { + validationErrors.push("Description is required"); + } + + // Check categories + if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) { + validationErrors.push("At least one category must be selected"); + } + + // Check navigation items + if (!formData.navigation.items || !Array.isArray(formData.navigation.items) || formData.navigation.items.length === 0) { + validationErrors.push("Please add at least one section"); + } else { + // Validate each section + for (let i = 0; i < formData.navigation.items.length; i++) { + const item = formData.navigation.items[i]; + + if (!item.name || item.name.trim() === "") { + validationErrors.push(`Section ${i+1} must have a name`); + } + + if (!item.content || !Array.isArray(item.content) || item.content.length === 0 || + !item.content.some(content => content && content.trim() !== "")) { + validationErrors.push(`Section "${item.name || i+1}" must have content`); + } + } + } + + // If there are validation errors, show the first one and return + if (validationErrors.length > 0) { + toast.error(validationErrors[0]); + return; + } + + // Set loading state to true + setIsSubmitting(true); + + // Get the image paths for source and destination app types + const srcImg = getCategoryImagePath(formData.mainContent.sourceAppType); + const dstImg = getCategoryImagePath(formData.mainContent.destinationAppType); + + const response = { + ...formData, + companyInfo: { + id: partnerData?.id, + name: partnerData?.name, + }, + } + + fetch(`${globalUrl}/api/v1/usecases/${formData.id}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(response), + }) + .then((res) => { + if (res.status !== 200) { + console.error("Failed to submit usecase"); + toast.error("Failed to submit usecase"); + setIsSubmitting(false); + return null; + } + return res.json(); + }) + .then((responseData) => { + console.log("Response data:", responseData); + // Reset loading state + setIsSubmitting(false); + + if (responseData) { + // Only close the dialog if the request was successful + const usecaseData = { + id: responseData.usecaseId.trim(), + sourceAppType: formData.mainContent?.sourceAppType.trim() || "", + destinationAppType: formData.mainContent?.destinationAppType.trim() || "", + srcImg: getCategoryImagePath(formData.mainContent?.sourceAppType.trim()), + dstImg: getCategoryImagePath(formData.mainContent?.destinationAppType.trim()), + publicWorkflowId: formData.mainContent?.publicWorkflowId.trim() || "", + name: formData.mainContent?.title.trim(), + description: formData.mainContent?.description.trim(), + categories: formData.mainContent?.categories || [], + navigation: formData.navigation || {}, + public: formData?.public || false, + } + + setUsecaseData((prevData) => { + // Handle case when prevData is null or undefined + if (!prevData) { + return [usecaseData]; + } + + // Check if the usecase already exists + const existingIndex = prevData.findIndex( + (usecase) => usecase.id === responseData.usecaseId + ); + if (existingIndex !== -1) { + // Update existing usecase + const updatedData = [...prevData]; + updatedData[existingIndex] = usecaseData; + return updatedData; + } else { + // Add new usecase + return [...prevData, usecaseData]; + } + }); + + setPartnerData((prevData) => { + // Handle case when prevData is null or undefined + if (!prevData) { + return { + usecases: [responseData.usecaseId.trim()] + }; + } + + return { + ...prevData, + usecases: [ + ...(prevData.usecases || []), + responseData.usecaseId.trim(), + ], + }; + }); + + // Show success message + if(formData?.id){ + toast.success("Usecase updated successfully"); + }else{ + toast.success("Usecase created successfully, publish it with toggle button"); + } + handleCloseDialog(); + } + }) + .catch((error) => { + console.error("Error submitting usecase:", error); + toast.error("Error submitting usecase"); + // Reset loading state on error + setIsSubmitting(false); + }); + }; + + return ( + + {/* Add Usecase Button */} + + + Usecases + + + + + {/* Usecases Grid */} + {isLoading ? ( + // Skeleton loading state + + {/* Display 4 skeleton cards while loading */} + {[...Array(4)].map((_, index) => ( + + ))} + + ) : usecaseData.length > 0 ? ( + // Actual data display + + {usecaseData.map((usecase) => ( + + ))} + + ) : ( + // Empty state + + + No usecases found + + + )} + + {/* Add Usecase Dialog */} + + + + {formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"} + + + + + + + + + + + Public Workflow + + + + + {/* App Type Selection */} + + + + Source App Type + + + + + + + Destination App Type + + + + + + + + Main Content + + + + + Title + + + setFormData({ + ...formData, + mainContent: { + ...formData.mainContent, + title: e.target.value, + }, + }) + } + sx={{ + "& .MuiOutlinedInput-root": { + height: 40, + backgroundColor: theme.palette.usecaseDialogFieldColor, + color: theme.palette.text.primary, + "& fieldset": { border: theme.palette.defaultBorder }, + }, + }} + /> + + + + Description + + + setFormData({ + ...formData, + mainContent: { + ...formData.mainContent, + description: e.target.value, + }, + }) + } + sx={{ + "& .MuiOutlinedInput-root": { + backgroundColor: theme.palette.usecaseDialogFieldColor, + p: 1.5, + color: theme.palette.text.primary, + "& fieldset": { border: theme.palette.defaultBorder }, + }, + }} + /> + + + + Categories + + + + + + + {/* Navigation Items Section */} + + + Navigation Items + + {formData.navigation.items.map((item, itemIndex) => ( + + + { + const newItems = [...formData.navigation.items]; + newItems[itemIndex].name = e.target.value; + setFormData({ + ...formData, + navigation: { items: newItems }, + }); + }} + placeholder="Section Name" + sx={{ + flex: 1, + "& .MuiOutlinedInput-root": { + height: 40, + backgroundColor: theme.palette.usecaseDialogFieldColor, + color: theme.palette.text.primary, + "& fieldset": { border: theme.palette.defaultBorder }, + }, + }} + /> + + { + if (itemIndex > 0) { + const newItems = [...formData.navigation.items]; + [newItems[itemIndex - 1], newItems[itemIndex]] = [ + newItems[itemIndex], + newItems[itemIndex - 1], + ]; + setFormData({ + ...formData, + navigation: { items: newItems }, + }); + } + }} + disabled={itemIndex === 0} + sx={{ + color: theme.palette.text.secondary, + "&:not(:disabled):hover": { + backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)", + }, + }} + > + + + { + if ( + itemIndex < + formData.navigation.items.length - 1 + ) { + const newItems = [...formData.navigation.items]; + [newItems[itemIndex], newItems[itemIndex + 1]] = [ + newItems[itemIndex + 1], + newItems[itemIndex], + ]; + setFormData({ + ...formData, + navigation: { items: newItems }, + }); + } + }} + disabled={ + itemIndex === formData.navigation.items.length - 1 + } + sx={{ + color: theme.palette.text.secondary, + "&:not(:disabled):hover": { + backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)", + }, + }} + > + + + { + const newItems = formData.navigation.items?.filter( + (_, i) => i !== itemIndex + ); + setFormData({ + ...formData, + navigation: { items: newItems }, + }); + }} + sx={{ + color: "#ff4444", + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(255, 68, 68, 0.08)" : "rgba(255, 68, 68, 0.15)", + }, + }} + > + + + + + + {item.content.map((content, contentIndex) => ( + + { + const newItems = [...formData.navigation.items]; + newItems[itemIndex].content[contentIndex] = + e.target.value; + setFormData({ + ...formData, + navigation: { items: newItems }, + }); + }} + placeholder="Content" + fullWidth + sx={{ + "& .MuiOutlinedInput-root": { + backgroundColor: theme.palette.usecaseDialogFieldColor, + color: theme.palette.text.primary, + "& fieldset": { border: theme.palette.defaultBorder }, + }, + }} + /> + {item.content.length > 1 && ( + + )} + + ))} + + + + ))} + + + + + + + + + + + + + ); +}; + +export default PartnersUsecasesTab; diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx index 5102b72b..639c6263 100644 --- a/frontend/src/components/Priorities.jsx +++ b/frontend/src/components/Priorities.jsx @@ -1,27 +1,38 @@ -import React, { useState, useEffect, useContext, memo } from "react"; - +import React, { useState, useEffect, useContext, memo, useCallback } from "react"; import { toast } from "react-toastify"; -import theme from "../theme.jsx"; +import { getTheme } from "../theme.jsx"; import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid"; import { Paper, Tooltip, - Typography, + Typography, Divider, Button, ButtonGroup, Grid, Card, - Chip, - Switch, + Switch, Autocomplete, TextField, MenuItem, IconButton, + Dialog, + FormControl, + DialogContent, + DialogActions, + DialogTitle, + InputLabel, + Box, + Select } from "@mui/material"; +import AuthenticationData from "../components/AuthenticationWindow.jsx"; +import Stack from "@mui/material/Stack"; +import Chip from "@mui/material/Chip"; + import { OpenInNew as OpenInNewIcon, + Info as InfoIcon, } from "@mui/icons-material"; import { makeStyles } from "@mui/styles"; @@ -40,19 +51,20 @@ const useStyles = makeStyles({ }); const Priorities = memo((props) => { - const { globalUrl, userdata,clickedFromOrgTab,selectedOrganization, handleEditOrg, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props; - - const [showDismissed, setShowDismissed] = React.useState(false); - const [showRead, setShowRead] = React.useState(false); - const [appFramework, setAppFramework] = React.useState({}); - const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT"); - const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT"); - const [highlightKMS, setHighlightKMS] = React.useState(false) + const { globalUrl, userdata, clickedFromOrgTab, selectedOrganization, handleEditOrg, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props; + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const [showDismissed, setShowDismissed] = React.useState(false); + const [showRead, setShowRead] = React.useState(false); + const [appFramework, setAppFramework] = React.useState({}); + const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT"); + const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT"); + const [highlightKMS, setHighlightKMS] = React.useState(false) - const [workflows, setWorkflows] = React.useState([]) - const [openNotification, setOpenNotification] = React.useState(false); - const [workflow, setWorkflow] = React.useState({}) - const [notificationWorkflow, setNotificationWorkflow] = React.useState( + const [workflows, setWorkflows] = React.useState([]) + const [openNotification, setOpenNotification] = React.useState(false); + const [workflow, setWorkflow] = React.useState({}) + const [notificationWorkflow, setNotificationWorkflow] = React.useState( selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.notification_workflow === undefined || @@ -60,9 +72,973 @@ const Priorities = memo((props) => { ? "" : selectedOrganization.defaults.notification_workflow ); - - let navigate = useNavigate(); - const classes = useStyles(); + + + // notification workflow + const [notificationWorkflowModal, setNotificationWorkflowModal] = React.useState(false); + const [selectedAppDetails, setSelectedAppDetails] = React.useState({}); + const [notificationWorkflowTestModal, setNotificationWorkflowTestModal] = React.useState(false); + const [selectedAuth, setSelectedAuth] = React.useState(''); + const [emailData, setEmailData] = React.useState([]); + const [notificationAppsDetails, setnotificationAppsDetails] = React.useState({}); + const [generatedWorkflow, setGeneatedWorkflow] = React.useState({}); + + // for jira & email modal + const [textFieldValue, setTextFieldValue] = React.useState(""); + const [textFieldOneValue, setTextFieldOneValue] = React.useState(""); + + useEffect(() => { + prepareNotificationAppList() + + if (notifications === undefined || notifications === null || notifications.length === 0) { + getNotifications() + } + }, []); + + let navigate = useNavigate(); + const classes = useStyles(); + + + // getting comms & cases app from app framework + var notificationAppList = []; + const mergeAuthData = (result, responseJson) => { + // result is only getting: Jira AND discord. + + const updatedResult = result.map(item => { + const matches = responseJson.filter(authItem => (authItem.app.name === item.name) + ); + return { + ...item, + authentication_data: matches.length > 0 ? matches : null + }; + }); + return updatedResult; + }; + + const prepareNotificationAppList = () => { + // getting App ID,Authentication fields and saved auths for each app + var result = [] + fetch(globalUrl + "/api/v1/apps", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + toast("Failed getting app ids: ", response.reason); + console.log("Status not 200 for app ids :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson !== undefined) { + const filteredApps = responseJson.filter(app => app.categories?.includes("Communication") || app.categories?.includes("Cases")); + const emailData = responseJson.filter(app => app.name === "email") + setEmailData(emailData) + const appDetails = filteredApps.map(app => ({ name: app.name, id: app.id })); //mapped apps with IDs as sometime Ids were not correct in security framework + // console.log("appDetails: ", appDetails) + // result = appDetails + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast(`Failed getting auth for : `, response.reason); + console.log("Status not 200 for app auth :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + if (!responseJson.success) { + console.log("Could not get app auth") + return; + } + // console.log("responseJson of auth: ", responseJson.data) + result = mergeAuthData(appDetails, responseJson.data) + // console.log("merged auth data: ", result) + // console.log("result", result) + + result.map(item => { + fetch(globalUrl + `/api/v1/apps/${item.id}/config`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + toast(`Failed getting config for ${item.id}: `, response.reason); + console.log("Status not 200 for app config :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + if (!responseJson.success) { + console.log("Could not get app config") + return; + } + var decodedString = JSON.parse(atob(responseJson.app)); + + // why is auth_config appearing like this? + // this is not very clean. + item.auth_config = decodedString.authentication + item.large_image = decodedString.large_image + item.categories = decodedString.categories + + let newJSON = notificationAppList; + + + // keeping state changes to a minimum + if ((newJSON[item.name] === undefined) && (item?.authentication_data !== null)) { + newJSON[item.name] = item; + setnotificationAppsDetails(newJSON); + console.log("notificationAppsDetails: ", newJSON) + } + }).then(() => { + // removing this for now, + // checkIfAlreadyGenerated(filteredApps, workflows); + + }).catch((error) => { + console.log("Error getting app config: " + error); + toast("Error getting app config: " + error); + }) + }) + }) + } + }).catch((error) => { + console.log("Error getting app ids: " + error); + }) + } + + + const executeTestWorkflow = async (workflowid) => { + const data = { "execution_argument": '{"title":"THIS IS TEST ALERT","description":"TEST ALERT FROM SHUFFLE","reference_url": "shuffler.io"}' } + fetch(globalUrl + `/api/v1/workflows/${workflowid}/execute`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed setting notification workflow: ", response.reason); + console.log("Status not 200 for workflows :O!"); + return; + } + toast("Notification workflow ran successfully"); + return response.json(); + }).catch((error) => { + console.log("Error getting workflows: " + error); + }) + } + + useEffect(() => { + console.log("Apps: ", notificationAppsDetails); + }, [notificationAppsDetails]) + + const generateCommsNotificationWorkflow = async (app, sender, recepient) => { + var appname = app.name + var appImage = app.large_image + + //currently only supports figure out a way to support more apps + var workflowName = `[GENERATED] ${appname} notification workflow` + var workflowDescription = "Generated by Shuffle for sending info/error notifications." + var data = { + "name": workflowName, + "description": workflowDescription, + } + + fetch(globalUrl + "/api/v1/workflows", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed setting notification workflow: ", response.reason); + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + toast("Trying to create notification workflow..") + if (responseJson !== undefined) { + console.log("Notification workflow created successfully") + + var commsParameter = [ + { + "key": "recipient", + "value": recepient + }, + { + "key": "sender", + "value": sender + }, + { + "key": "subject", + "value": "$exec.title" + }, + { + "key": "body", + "value": "$exec.description" + } + ] + + if (app.name.toLowerCase() === "gmail") { + // this is a special case and we know it + commsParameter = `[ +{ +"key": "content", +"value": {% python %} +import base64 + +# Set your email variables +sender = "${sender}" +recipient = "${recepient}" +subject = "$exec.title" +body = "$exec.description" + +raw_email = f"""FROM: {sender}\\nTO: {recipient}\\nsubject: {subject}\\n{body}""" + +# Encode for Gmail API +encoded = base64.urlsafe_b64encode(raw_email.encode()).decode() + +print('"' + encoded + '"') +{% endpython %}, +}, +{ +"key": "userId", +"value": "me" +}]` + } + + var workflow_id = responseJson.id; + + + var commsAction = { + "app_name": "Singul", + "app_version": "1.0.0", + "description": "Available actions for communication", + "app_id": "integration", + "errors": [], + "is_valid": true, + "isStartNode": true, + "label": "Send Notification Message", + "public": false, + "generated": false, + "large_image": app.large_image, + "environment": "Cloud", + "name": "Communication", + "parameters": [ + { + "description": "", + "id": "", + "name": "action", + "example": "", + "value": "send_message", + "multiline": false, + "multiselect": false, + "options": [ + "list_messages", + "get_message", + "send_message", + "search_messages", + "list_attachments", + "get_attachment", + "create_contact", + "get_contact" + ], + "action_field": "", + "variant": "", + "required": true, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + }, + { + "description": "", + "id": "", + "name": "fields", + "example": "", + "value": `${commsParameter}`, + "multiline": true, + "multiselect": false, + "options": null, + "action_field": "", + "variant": "", + "required": false, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + }, + { + "description": "", + "id": "", + "name": "app_name", + "example": "", + "value": app.name, + "multiline": false, + "multiselect": false, + "options": null, + "action_field": "", + "variant": "", + "required": false, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + } + ], + "execution_variable": { + "description": "", + "id": "", + "name": "", + "value": "" + }, + "position": { + "x": 67.96428571428578, + "y": 263.14385714285714 + }, + "authentication_id": "", + "category": "", + "reference_url": "", + "sub_action": false, + "run_magic_output": false, + "run_magic_input": false, + "execution_delay": 0, + "category_label": null, + "suggestion": false, + "parent_controlled": false, + "source_workflow": "", + "source_execution": "" + } + + console.log("updating workflow for email") + var workflowBody = { + "name": workflowName, + "Description": workflowDescription, + "id": workflow_id, + "actions": [ + commsAction, + ] + } + + fetch(globalUrl + `/api/v1/workflows/${workflow_id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(workflowBody), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed setting notification workflow: ", response.reason); + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson !== undefined) { + toast("Saving as notification workflow now..") + handleEditOrg( + selectedOrganization.name, + selectedOrganization.description, + selectedOrganization.id, + selectedOrganization.image, + { + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + notification_workflow: workflow_id, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + + console.log("Notification workflow updated successfully") + toast("Notification workflow updated successfully") + } + }) + } + }).catch((error) => { + console.log("Error setting workflows: " + error); + }) + } + + const generateCasesNotificationWorkflow = async (app, projectId) => { + var appname = app.name + var appImage = app.large_image + + //currently only supports figure out a way to support more apps + var workflowName = `[GENERATED] ${appname} notification workflow` + var workflowDescription = "Generated by Shuffle for sending info/error notifications." + var data = { + "name": workflowName, + "description": workflowDescription, + } + + fetch(globalUrl + "/api/v1/workflows", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed setting notification workflow: ", response.reason); + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + toast("Trying to create notification workflow..") + if (responseJson !== undefined) { + console.log("Notification workflow created successfully") + + var ticketParameter = [ + { + "key": "project", + "value": projectId, + }, + { + "key": "content", + "value": "$exec.title" + }, + { + "key": "title", + "value": "$exec.description" + } + ] + + // fields = fields.concat(otherFields) + + var workflow_id = responseJson.id; + + var commsAction = { + "app_name": "Singul", + "app_version": "1.0.0", + "description": "Available actions for case management", + "app_id": "integration", + "errors": [ + + ], + "is_valid": true, + "isStartNode": true, + "label": "create_case_from_notification", + "public": false, + "generated": false, + "large_image": app.large_image, + "environment": "Cloud", + "name": "Cases", + "parameters": [ + { + "description": "", + "id": "", + "name": "action", + "example": "", + "value": "create_ticket", + "multiline": false, + "multiselect": false, + "options": [ + "list_tickets", + "get_ticket", + "create_ticket", + "close_ticket", + "add_comment", + "update_ticket", + "search_tickets" + ], + "action_field": "", + "variant": "", + "required": true, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + }, + { + "description": "", + "id": "", + "name": "fields", + "example": "", + "value": ticketParameter, + "multiline": true, + "multiselect": false, + "options": null, + "action_field": "", + "variant": "", + "required": false, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + }, + { + "description": "", + "id": "", + "name": "app_name", + "example": "", + "value": app.name, + "multiline": false, + "multiselect": false, + "options": null, + "action_field": "", + "variant": "", + "required": false, + "configuration": false, + "tags": null, + "schema": { + "type": "" + }, + "skip_multicheck": false, + "value_replace": null, + "unique_toggled": false, + "error": "", + "hidden": false + } + ], + "execution_variable": { + "description": "", + "id": "", + "name": "", + "value": "" + }, + "position": { + "x": -111.59751960534861, + "y": 180.34429863506278 + }, + "authentication_id": "", + "category": "", + "reference_url": "", + "sub_action": false, + "run_magic_output": false, + "run_magic_input": false, + "execution_delay": 0, + "category_label": null, + "suggestion": false, + "parent_controlled": false, + "source_workflow": "", + "source_execution": "" + } + + console.log("updating workflow for email") + var workflowBody = { + "name": workflowName, + "Description": workflowDescription, + "id": workflow_id, + "actions": [ + commsAction, + ] + } + + fetch(globalUrl + `/api/v1/workflows/${workflow_id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(workflowBody), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + toast("Failed setting notification workflow: ", response.reason); + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson !== undefined) { + handleEditOrg( + selectedOrganization.name, + selectedOrganization.description, + selectedOrganization.id, + selectedOrganization.image, + { + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + notification_workflow: workflow_id, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + + console.log("Notification workflow updated successfully") + toast("Notification workflow updated successfully") + } + }) + } + }).catch((error) => { + console.log("Error setting workflows: " + error); + }) + } + + const testWorkflowModal = notificationWorkflowTestModal ? + ( { + setNotificationWorkflowTestModal(false); + }} + > + + {/* +
+ Notification workflow +
+
*/} + + We have updated the Notification workflow. Do you want to test it? + + + + + +
+
) : null + + const modalView = notificationWorkflowModal ? ( + { + setNotificationWorkflowModal(false); + }} + > + + +
+ {`Configure ${selectedAppDetails?.name?.replaceAll("_", " ")} workflow`} +
+
+ + + {console.log("len Selected app details: ", selectedAppDetails)} + + {(selectedAppDetails.authentication_data || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false) || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ? + <> + + {true || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ? "No authentication required" : + <> + + Pick an authentication method from the list + + + Available authentications + + } + + + + Provide additional required details: + + { + setTextFieldOneValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + { + setTextFieldValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + + : + <> + 0) ? false : true} + // // setAuthenticationModalOpen={false} + selectedApp={{ ...selectedAppDetails, authentication: selectedAppDetails.auth_config }} + // getAppAuthentication={selectedAppDetails.name} + /> + + } + + + + + +
+
+ ) : null + + + const checkIfAlreadyGenerated = async (appList, workflows) => { // fixxxxxxxxxxxxxxxxxxxxx + + var workflowName = workflows.find(workflow => workflow.id === notificationWorkflow) + if (workflowName) { + workflowName = workflowName.name + } + else { + console.log("no workflow set") + return + } + if (workflowName) { + const parts = workflowName.split(' '); + console.log("parts", parts) + if (parts[0].toString() === "[GENERATED]" && parts.length > 1) { + console.log("parts1", parts[1]) + if ((appList.includes(parts[1]))) { + console.log("workflow already generated") + setGeneatedWorkflow({ "app_name": parts[1] }) + } + } + } + else { + return + } + } + + const renderChips = useCallback(() => { + const appList = Object.values(notificationAppsDetails); + + return ( + + + + {appList.map((app) => ( + { + console.log(`Clicked ${app.name}`) + console.log("app: ", app) + setSelectedAppDetails(app) + if (app.authentication_data && app.authentication_data.length > 0) { //fixxxxxxxx + console.log("authdata: ", app.authentication_data[0]) + setSelectedAuth(app.authentication_data[app.authentication_data.length - 1].id) + } + setNotificationWorkflowModal(true) + // getAppAuth(app.name) + console.log("selectedAppDEtails", selectedAppDetails) + }} + avatar={{app.name}} + /> + ))} + + + + + + + Want access to more templates? + + Set up app authentication + + to show additional workflow options. + + + + ); + }, [notificationAppsDetails]) useEffect(() => { getFramework() @@ -82,7 +1058,7 @@ const Priorities = memo((props) => { setSelectedExecutionId(execution_id) //toast.info("Execution-related notifications are highlighted.") - } + } if (workflow !== null) { setSelectedWorkflow(workflow) @@ -96,7 +1072,7 @@ const Priorities = memo((props) => { return } - if(workflows?.length === 0) { + if (workflows?.length === 0) { getAvailableWorkflows() } @@ -106,7 +1082,7 @@ const Priorities = memo((props) => { }, [selectedOrganization]) if (userdata === undefined || userdata === null) { - return + return } const getFramework = () => { @@ -118,67 +1094,95 @@ const Priorities = memo((props) => { }, credentials: "include", }) - .then((response) => { + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + setAppFramework({}) + if (responseJson.reason !== undefined) { + //toast("Failed loading: " + responseJson.reason) + } else { + //toast("Failed to load framework for your org.") + } + } else { + setAppFramework(responseJson) + } + }) + .catch((error) => { + console.log("err in framework: ", error.toString()); + }) + } + + const getNotifications = () => { + fetch(`${globalUrl}/api/v1/notifications`, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then(function (response) { if (response.status !== 200) { - console.log("Status not 200 for framework!"); + console.log("Error in response"); } return response.json(); }) - .then((responseJson) => { - if (responseJson.success === false) { - setAppFramework({}) - if (responseJson.reason !== undefined) { - //toast("Failed loading: " + responseJson.reason) - } else { - //toast("Failed to load framework for your org.") - } + .then(function (responseJson) { + if (responseJson?.success !== false && responseJson?.notifications !== undefined && responseJson?.notifications !== null) { + setNotifications(responseJson.notifications || []) } else { - setAppFramework(responseJson) + toast("Failed loading notifications. Please try again later."); } }) .catch((error) => { - console.log("err in framework: ", error.toString()); - }) + console.log("error in notification loading: ", error); + }); + } - const clearNotifications = () => { - // Don't really care about the logout + const clearNotifications = () => { + // Don't really care about the logout - toast("Clearing notifications") - fetch(`${globalUrl}/api/v1/notifications/clear`, { - credentials: "include", - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } + toast.info("Marking all notifications as read. This may take a while.") + fetch(`${globalUrl}/api/v1/notifications/clear`, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - // Reload the UI - const newNotifications = notifications.map((notification) => { - notification.read = true - return notification - }) + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + // Reload the UI + const newNotifications = notifications.map((notification) => { + notification.read = true + return notification + }) - setNotifications(newNotifications) - setShowRead(true) - } else { - toast("Failed dismissing notifications. Please try again later."); - } - }) - .catch((error) => { - console.log("error in notification dismissal: ", error); - //removeCookie("session_token", {path: "/"}) - }); - }; + setNotifications(newNotifications) + setShowRead(true) + } else { + toast("Failed dismissing notifications. Please try again later."); + } + }) + .catch((error) => { + console.log("error in notification dismissal: ", error); + //removeCookie("session_token", {path: "/"}) + }); + }; const dismissNotification = (alert_id, disabled) => { var notificationurl = `${globalUrl}/api/v1/notifications/${alert_id}/markasread` @@ -188,82 +1192,82 @@ const Priorities = memo((props) => { notificationurl += "?disabled=false" } - fetch(notificationurl , { - credentials: "include", - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } - - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - // Mark current one as read - var newNotifications = notifications.map((notification) => { - if (notification.id === alert_id) { - notification.read = true + fetch(notificationurl, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); } - return notification + return response.json(); }) + .then(function (responseJson) { + if (responseJson.success === true) { + // Mark current one as read + var newNotifications = notifications.map((notification) => { + if (notification.id === alert_id) { + notification.read = true + } + + return notification + }) - if (disabled === true) { - toast("Notification disabled, and will not be shown again.") + if (disabled === true) { + toast("Notification disabled, and will not be shown again.") - newNotifications = newNotifications.map((notification) => { - if (notification.id === alert_id) { - notification.ignored = true + newNotifications = newNotifications.map((notification) => { + if (notification.id === alert_id) { + notification.ignored = true + } + + return notification + }) + + console.log("NEW NOTIFICATIONS: ", newNotifications); + } else if (disabled === false) { + toast("Notification re-enabled successfully") + + newNotifications = newNotifications.map((notification) => { + if (notification.id === alert_id) { + notification.ignored = false + } + + return notification + }) + + } else { + toast("Notification dismissed successfully") } - return notification - }) + //const newNotifications = notifications.filter( + // (data) => data.id !== alert_id + //) - console.log("NEW NOTIFICATIONS: ", newNotifications); - } else if (disabled === false) { - toast("Notification re-enabled successfully") + //console.log("NEW NOTIFICATIONS: ", newNotifications); - newNotifications = newNotifications.map((notification) => { - if (notification.id === alert_id) { - notification.ignored = false + if (setNotifications !== undefined && newNotifications !== undefined) { + setNotifications(newNotifications) } - - return notification - }) - - } else { - toast("Notification dismissed successfully") - } - - //const newNotifications = notifications.filter( - // (data) => data.id !== alert_id - //) - - //console.log("NEW NOTIFICATIONS: ", newNotifications); - - if (setNotifications !== undefined && newNotifications !== undefined) { - setNotifications(newNotifications) - } - } else { - toast("Failed dismissing notification. Please try again later."); - } - }) - .catch((error) => { - console.log("error in notification dismissal: ", error); - //removeCookie("session_token", {path: "/"}) - }) + } else { + toast("Failed dismissing notification. Please try again later."); + } + }) + .catch((error) => { + console.log("error in notification dismissal: ", error); + //removeCookie("session_token", {path: "/"}) + }) } - - const notificationWidth = "100%" + + const notificationWidth = "100%" const imagesize = 22 - const boxColor = "#86c142" + const boxColor = "#86c142" const getAvailableWorkflows = () => { @@ -353,131 +1357,168 @@ const Priorities = memo((props) => { } return ( -
-
-
- - Notification Workflow - - - The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. You can point child org notifications into the parent org notification by choosing it in the list. - - -
+
+
+
+ + Notification Workflow + + + The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. You can point child org notifications into the parent org notification by choosing it in the list. + - {workflows !== undefined && workflows !== null && workflows.length > 0 ? - { - setOpenNotification(true); - }} - onClose={() => { - setOpenNotification(false); - }} - freeSolo - //autoSelect - value={workflows?.find(w => w.id === notificationWorkflow) || null} - classes={{ inputRoot: classes.inputRoot }} - ListboxProps={{ - style: { - backgroundColor: "#212121", - color: "white", - }, - }} - getOptionLabel={(option) => { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return "No Workflow Selected"; - } + {modalView} + {/*{testWorkflowModal} */} +
+ {renderChips()} +
- const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={workflows} - fullWidth - style={{ - backgroundColor: "#212121", - borderRadius: theme.palette?.borderRadius, - height: 35, - marginBottom: 40, - }} - onChange={(event, newValue) => { - console.log("Found value: ", newValue) +
- var parsedinput = { target: { value: newValue } } - - // For variables - if (typeof newValue === 'string' && newValue.startsWith("$")) { - parsedinput = { - target: { - value: { - "name": newValue, - "id": newValue, - "actions": [], - "triggers": [], - } - } - } - } - - handleWorkflowSelectionUpdate(parsedinput) - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } - - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose {data.name} - - - } placement="bottom"> - { - props.onMouseDown?.(null); - var parsedinput = { target: { value: data } } - handleWorkflowSelectionUpdate(parsedinput) - }} - > - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - 0 ? + { + setOpenNotification(true); }} + onClose={() => { + setOpenNotification(false); + }} + freeSolo + //autoSelect + value={workflows?.find(w => w.id === notificationWorkflow) || null} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: theme.palette.text.primary, + borderRadius: theme.palette.borderRadius, + }, + }} + 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.textFieldStyle.backgroundColor, + borderRadius: theme.palette.textFieldStyle.borderRadius, + color: theme.palette.textFieldStyle.color, + height: 35, + marginBottom: 40, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + props.onMouseDown?.(null); + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { borderRadius: 4, }, inputProps: { - ...params.inputProps, style: { height: "100%", boxSizing: "border-box", @@ -495,197 +1535,171 @@ const Priorities = memo((props) => { } }} - // label="Find a notification workflow" - variant="outlined" - placeholder="Select a notification workflow" + style={{ + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, + borderRadius: 4, + height: 35, + fontSize: 16, + marginBottom: 30 + }} + fullWidth={true} + type="name" + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="ID of the workflow to receive notifications" + value={notificationWorkflow} + onChange={(e) => { + setNotificationWorkflow(e.target.value); + }} /> - ); - }} - /> - : - { - setNotificationWorkflow(e.target.value); - }} - /> - } - {/*
+ {/*
{orgSaveButton}
*/} -
+
- {notificationWorkflow === undefined || notificationWorkflow === null || notificationWorkflow.length === 0 ? null : -
- + { + if (notificationWorkflow === "parent") { + toast.error("Can't open parent org's notification workflow from here.") + return + } + + window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank") + }} + > + + +
} - fetch(`${globalUrl}/api/v1/workflows/${notificationWorkflow}/execute`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - "title": "Test Notification", - "description": "This is a test notification to check if the notification workflow is working correctly.", - "org_id": selectedOrganization.id, - "id": uuidv4(), - "reference_url": "/admin?type=test&admin_tab=notifications", - "created_at": Math.floor(new Date().getTime() / 1000), - "updated_at": Math.floor(new Date().getTime() / 1000), + Notifications ({ + notifications?.filter((notification) => showRead === true || notification.read === false).length + }) + + + Notifications help you find potential problems with your workflows and apps.  + + Learn more + + +
+
+ { + setShowRead(!showRead); + }} + />  Show read + {notifications !== undefined && notifications !== null && notifications.length > 1 ? ( + + ) : null} +
+ + + + {clickedFromOrgTab ? null : } + + Suggestions + + Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
These range from simple configurations in Shuffle to Usecases you may have missed.  + + Learn more + +
+
+ { + setShowDismissed(!showDismissed); + }} + />  Show dismissed + {userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ? + + No Suggestions found + + : + userdata.priorities.map((priority, index) => { + if (showDismissed === false && priority.active === false) { + return null + } + + return ( + + ) }) - }) - .then((response) => { - if (response.status === 200) { - toast.success("Test notification sent successfully.") - } else { - toast.error("Failed to send test notification. Please contact support if this persists") - } - }).catch((error) => { - toast.error("Failed to send test notification (2). Please contact support if this persists") - }) - }}> - Send test notification - - { - if (notificationWorkflow === "parent") { - toast.error("Can't open parent org's notification workflow from here.") - return - } - - window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank") - }} - > - - -
- } - - Notifications ({ - notifications?.filter((notification) => showRead === true || notification.read === false).length - }) - - - Notifications help you find potential problems with your workflows and apps.  - - Learn more - - -
-
- { - setShowRead(!showRead); - }} - />  Show read - {notifications !== undefined && notifications !== null && notifications.length > 1 ? ( - - ) : null} -
- - - - {clickedFromOrgTab? null : } - -

Suggestions

- - Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
These range from simple configurations in Shuffle to Usecases you may have missed.  - - Learn more - -
-
- { - setShowDismissed(!showDismissed); - }} - />  Show dismissed - {userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ? - - No Suggestions found - - : - userdata.priorities.map((priority, index) => { - if (showDismissed === false && priority.active === false) { - return null } - - return ( - - ) - }) - } -
-
+
+
) }) @@ -694,14 +1708,15 @@ export default Priorities; const NotificationItem = memo((props) => { - const {data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification} = props + const { data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification } = props var image = ""; var orgName = ""; var orgId = ""; + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); - - var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) + var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow) if (!highlighted && highlightKMS) { if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) { @@ -713,238 +1728,242 @@ const NotificationItem = memo((props) => { } if (userdata.orgs !== undefined) { - const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); - if (foundOrg !== undefined && foundOrg !== null) { - //position: "absolute", bottom: 5, right: -5, - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - marginLeft: - data.creator_org !== undefined && data.creator_org.length > 0 - ? 20 - : 0, - borderRadius: 10, - border: - foundOrg.id === userdata.active_org.id - ? `3px solid ${boxColor}` - : null, - cursor: "pointer", - marginRight: 10, - }; + const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); + if (foundOrg !== undefined && foundOrg !== null) { + //position: "absolute", bottom: 5, right: -5, + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginLeft: + data.creator_org !== undefined && data.creator_org.length > 0 + ? 20 + : 0, + borderRadius: 10, + border: + foundOrg.id === userdata.active_org.id + ? `3px solid ${boxColor}` + : null, + cursor: "pointer", + marginRight: 10, + }; - image = - foundOrg.image === "" ? ( - {foundOrg.name} - ) : ( - {foundOrg.name} {}} - /> - ); + image = + foundOrg.image === "" ? ( + {foundOrg.name} + ) : ( + {foundOrg.name} { }} + /> + ); - orgName = foundOrg.name; - orgId = foundOrg.id; - } + orgName = foundOrg.name; + orgId = foundOrg.id; + } } return ( - -
- {data.amount === 1 && data.read === false ? - - : null} - {data.ignored === true ? - - : null} - {data.read === false ? - - : - - } - - {data.title} - -
- - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.title} - : - null - } - - {data.description} - -
- - - - {data.read === false ? ( - - ) : null} - - - - - +
+ {data.amount === 1 && data.read === false ? + + : null} + {data.ignored === true ? + + : null} + {data.read === false ? + + : + + } + + {data.title} + +
- 0 ? + {data.title} + : + null + } + + {data.description} + +
+ + - }} - > - First seen:{" "} - {new Date(data.created_at * 1000).toISOString().slice(0, 19)} - + {data.read === false ? ( + + ) : null} - - Last seen:{" "} - {new Date(data.updated_at * 1000).toISOString().slice(0, 19)} - + + + + - - Times seen: {data.amount} - -
+ + }} + > + First seen:{" "} + {new Date(data.created_at * 1000).toISOString().slice(0, 19)} + + + + Last seen:{" "} + {new Date(data.updated_at * 1000).toISOString().slice(0, 19)} + + + + Times seen: {data.amount} + +
+ + ); }) -const NotificationComponent = memo(({notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification}) => { +const NotificationComponent = memo(({ notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification }) => { - return( + return (
{notifications === null || notifications === undefined || notifications?.length === 0 ? ( - null - ) : -
- {notifications?.map((notification, index) => { - if (showRead === false && notification.read === true) { - return null - } + null + ) : +
+ {notifications?.map((notification, index) => { + if (showRead === false && notification.read === true) { + return null + } - return ( - - ) - })} -
- } + return ( + + ) + })} +
+ }
) }) diff --git a/frontend/src/components/Priority.jsx b/frontend/src/components/Priority.jsx index 4bd3a6f2..7cc0f3da 100644 --- a/frontend/src/components/Priority.jsx +++ b/frontend/src/components/Priority.jsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { toast } from 'react-toastify'; - +import { getTheme } from "../theme.jsx"; import ReactGA from 'react-ga4'; -import theme from "../theme.jsx"; import { useNavigate, Link } from "react-router-dom"; import { findSpecificApp } from "../components/AppFramework.jsx" +import { Context } from "../context/ContextApi.jsx"; import { Paper, Typography, @@ -23,7 +23,8 @@ import { const Priority = (props) => { const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props; - + const { themeMode, supportEmail } = useContext(Context); + const theme = getTheme(themeMode); const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); let navigate = useNavigate(); @@ -113,7 +114,7 @@ const Priority = (props) => { } }) .catch((error) => { - toast("Failed dismissing alert. Please contact support@shuffler.io if this persists."); + toast(`Failed dismissing alert. Please contact ${supportEmail} if this persists.`); }); } @@ -121,10 +122,10 @@ const Priority = (props) => { const srcSize = realignedSrc ? 35 : 30 const dstSize = realignedDst ? 35 : 30 return ( -
+
- {priority.type === "usecase" || priority.type == "apps" ? : null} + {priority.type === "usecase" || priority.type == "apps" ? : null} {priority.name} @@ -138,7 +139,7 @@ const Priority = (props) => { {newdescription.split("&").length > 3 ? - + {priority.name+"2"} {newdescription.split("&")[2]} @@ -154,7 +155,7 @@ const Priority = (props) => { }
- {priority.active === true ? -
} /> {/* @@ -763,13 +788,20 @@ const SearchData = props => { window.open(modifiedUrl, '_blank'); }; return ( - + Discord Chat - + {hits.length === 0 ? - + console.log(hits)}> @@ -781,7 +813,11 @@ const SearchData = props => { /> : hits.map((chat, index) => ( - handleHitClick(chat.url)} key={index} style={{ cursor: "pointer", borderBottom: "1px solid rgba(255,255,255,0.4)" }} onMouseOver={() => setMouseHoverIndex(index)}> + handleHitClick(chat.url)} key={index} style={{ cursor: "pointer", borderBottom: "1px solid rgba(255,255,255,0.4)" }} onMouseOver={() => setMouseHoverIndex(index)}> @@ -897,28 +933,38 @@ const SearchData = props => { const modalView = (
- - - - - + { + !isDocSearchModal ? ( + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + ) : ( + + + + + + ) + }
) @@ -932,7 +978,7 @@ const SearchData = props => { {modalView} - {gettingStartData} + {!isDocSearchModal && gettingStartData}
) } diff --git a/frontend/src/components/Searchfield.jsx b/frontend/src/components/Searchfield.jsx index ec257cc6..4447c73b 100644 --- a/frontend/src/components/Searchfield.jsx +++ b/frontend/src/components/Searchfield.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react'; -import theme from '../theme.jsx'; +import {getTheme} from '../theme.jsx'; import { useNavigate, Link, useParams } from "react-router-dom"; import SearchBox from "../components/SearchData.jsx"; @@ -46,7 +46,8 @@ const chipStyle = { const SearchField = props => { const { serverside, userdata, isMobile, isLoaded, globalUrl, isHeader, isLoggedIn, small, rounded } = props - + const {themeMode} = useContext(Context); + const theme = getTheme(themeMode); const {searchBarModalOpen, setSearchBarModalOpen} = useContext(Context); let navigate = useNavigate(); @@ -87,7 +88,7 @@ const SearchField = props => { height: 785, borderRadius: 16, border: "1px solid var(--Container-Stroke, #494949)", - background: "var(--Container, #000000)", + background: theme.palette.DialogStyle.backgroundColor, boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", zIndex: 13000, }, diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 1f713c5a..043c5de2 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect, useLayoutEffect, } from 'react'; +import React, { useRef, useState, useEffect, useLayoutEffect, useContext } from 'react'; import { toast } from 'react-toastify'; import '../codeeditor-index.css'; import { @@ -17,8 +17,8 @@ import { ButtonGroup, Collapse, } from '@mui/material'; - -import theme from '../theme.jsx'; +import { Context } from '../context/ContextApi.jsx'; +import {getTheme} from '../theme.jsx'; import Checkbox from '@mui/material/Checkbox'; import { isMobile } from "react-device-detect" import { NestedMenuItem } from "mui-nested-menu" @@ -46,6 +46,7 @@ import { DragIndicator as DragIndicatorIcon, RestartAlt as RestartAltIcon, ArrowForward as ArrowForwardIcon, + KeyboardReturn as KeyboardReturnIcon, } from '@mui/icons-material'; @@ -63,6 +64,7 @@ import AceEditor from "react-ace"; import ace from "ace-builds"; import 'ace-builds/src-noconflict/mode-python'; import 'ace-builds/src-noconflict/mode-json'; +import 'ace-builds/src-noconflict/mode-yaml'; //import 'ace-builds/src-noconflict/theme-twilight'; //import 'ace-builds/src-noconflict/theme-solarized_dark'; import 'ace-builds/src-noconflict/theme-gruvbox'; @@ -89,13 +91,17 @@ const liquidFilters = [ const pythonFilters = [ { "name": "Hello World", "value": `print("hello world")`, "example": `` }, { "name": "Using Shuffle variables", "value": `import json\nnodevalue = r\"\"\"$exec\"\"\"\nif not nodevalue:\n nodevalue = r\"\"\"{\"sample\": \"string\", \"int\": 1}\"\"\"\n \njsondata = json.loads(nodevalue)\nprint(jsondata)`, "example": `` }, + { "name": "Filter a list", "value": `import json\nnodevalue = r\"\"\"$exec\"\"\"\nif not nodevalue:\n nodevalue = r\"\"\"[{\"sample\": \"string\", \"int\": 1, "malicious": "no"}, {\"sample\": \"string2\", \"int\": 1, "malicious": "yes"}]\"\"\"\n \njsondata = json.loads(nodevalue)\nfiltered = []\nfor item in jsondata:\n try:\n if item[\"malicious\"] == \"yes\":\n filtered.append(item)\n except:\n pass\nprint(json.dumps(filtered))`, "example": `` }, { "name": "Print Execution ID", "value": `print(self.current_execution_id)`, "example": `` }, { "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, }, + ] @@ -114,6 +120,7 @@ const CodeEditor = (props) => { handleActionParamChange, setcodedata, isFileEditor, + isWorkflowEditor, runUpdateText, toolsAppId, parameterName, @@ -127,7 +134,7 @@ const CodeEditor = (props) => { fieldname, contentLoading, editorData, - handleSubflowParamChange, + handleTriggerParamChange, setAiQueryModalOpen, fullScreenMode, environment, @@ -138,12 +145,9 @@ const CodeEditor = (props) => { const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata); - //const { setContainer } = useCodeMirror({ - // container: editorRef.current, - // extensions, - // value: localcodedata, - //}) // const {codelang, setcodelang} = props + const {themeMode, supportEmail} = useContext(Context) + const theme = getTheme(themeMode) const [validation, setValidation] = React.useState(false); const [expOutput, setExpOutput] = React.useState(" "); @@ -205,7 +209,7 @@ const CodeEditor = (props) => { const triggerField = searchParams.get('trigger_field'); const triggerName = searchParams.get('trigger_name'); const conditionId = searchParams.get('condition_id'); - const conditionField = searchParams.get('field'); + const conditionField = searchParams.get('condition_field'); useEffect(() => { if (actionId === undefined || actionId === null) { @@ -244,7 +248,7 @@ const CodeEditor = (props) => { setSelectedCondition(condition); // Update available variables when condition changes updateAvailableVariables(actionlist); - }, [conditionId, fieldName]) + }, [conditionId, conditionField]) // Extract variable updating logic into a separate function const updateAvailableVariables = (actionlist) => { @@ -298,8 +302,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) }, []) @@ -886,6 +901,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 { @@ -920,28 +937,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) { @@ -1334,7 +1395,7 @@ const CodeEditor = (props) => { const usedposition = e.offsetY if (usedposition === undefined || usedposition === null) { - toast.info("Error: LayerY is undefined or null. Please contact support@shuffler.io") + toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`) return } @@ -1461,17 +1522,19 @@ const CodeEditor = (props) => { setActiveDialog("codeeditor") } }, - style: { + sx: { // zIndex: 12501, pointerEvents: "auto", - color: "white", - minWidth: isMobile ? "100%" : isFileEditor ? 650 : "80%", - maxWidth: isMobile ? "100%" : isFileEditor ? 650 : 1100, - minHeight: isMobile ? "100%" : "auto", - maxHeight: isMobile ? "100%" : 700, + color: theme.palette.DialogStyle.color, + minWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "80%", + maxWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "1100px", + minHeight: isMobile || isWorkflowEditor ? "100%" : "auto", + maxHeight: isMobile || isWorkflowEditor ? "100%" : "700px", border: "3px solid rgba(255,255,255,0.3)", - padding: isMobile ? "25px 10px 25px 10px" : 25, - backgroundColor: "black", + padding: isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px", + backgroundColor: themeMode === "dark" ? "black" : theme.palette.DialogStyle.backgroundColor, + + opacity: isWorkflowEditor ? 0.93 : 1, }, }} > @@ -1522,7 +1585,10 @@ const CodeEditor = (props) => { color: "grey", }} onClick={() => { - navigate("") + if (isFileEditor !== true) { + navigate("") + } + setExpansionModalOpen(false) }} > @@ -1652,7 +1718,7 @@ const CodeEditor = (props) => { Code Editor */} - {isFileEditor ? null : + {isFileEditor || isWorkflowEditor ? null :
{userdata !== undefined && userdata !== null && userdata.support === true ? @@ -1666,6 +1732,7 @@ const CodeEditor = (props) => { style={{ textTransform: "none", width: 175, + textWrap: 'nowrap' }} onClick={(event) => { setSourceDataOpen(!sourceDataOpen) @@ -1685,6 +1752,7 @@ const CodeEditor = (props) => { style={{ textTransform: "none", width: 120, + textWrap: "nowrap" }} onClick={(event) => { setAnchorEl(event.currentTarget); @@ -1720,13 +1788,14 @@ const CodeEditor = (props) => { color="secondary" style={{ textTransform: "none", - width: 120, + width: 145, + textWrap: "nowrap" }} onClick={(event) => { setAnchorEl3(event.currentTarget); }} > - Python Code + Python Examples
- {isFileEditor ? null : + {isFileEditor || isWorkflowEditor ? null :
{ paddingLeft: 10, paddingTop: 0, display: "flex", - cursor: "move" + cursor: "move", + color: theme.palette.DialogStyle.color, + backgroundColor: "transparent", }} >
@@ -2227,7 +2304,7 @@ const CodeEditor = (props) => { { selectedEdge && Object.keys(selectedEdge).length > 0 ? { }
: - + {selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : triggerId ? @@ -2269,9 +2346,8 @@ const CodeEditor = (props) => {
@@ -2428,7 +2523,7 @@ const CodeEditor = (props) => {
-
+
+
+
+ + ); + }; + const modalView = ( { }} > - Add Sub-Organization + Add Sub-Organization
@@ -616,7 +855,7 @@ const TenantsTab = memo((props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -635,15 +874,14 @@ const TenantsTab = memo((props) => {
@@ -850,7 +1087,7 @@ const TenantsTab = memo((props) => { /> */} -
+
{ display: "table-cell", padding: "0px 8px 8px 8px", textAlign: "center", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, verticalAlign: "middle", }} /> @@ -894,7 +1131,7 @@ const TenantsTab = memo((props) => { padding: "0px 8px 8px 8px", whiteSpace: "nowrap", textOverflow: "ellipsis", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, verticalAlign: "middle", textAlign: "center", }} @@ -906,7 +1143,7 @@ const TenantsTab = memo((props) => { minWidth: 100, maxWidth: 100, display: "table-cell", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, padding: "0px 8px 8px 8px", }} /> @@ -920,7 +1157,7 @@ const TenantsTab = memo((props) => { padding: "0px 8px 8px 8px", whiteSpace: "nowrap", textOverflow: "ellipsis", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, verticalAlign: "middle", }} /> @@ -933,7 +1170,7 @@ const TenantsTab = memo((props) => { padding: "0px 8px 8px 8px", whiteSpace: "nowrap", textOverflow: "ellipsis", - borderBottom: "1px solid #494949", + borderBottom: theme.palette.defaultBorder, verticalAlign: "middle", }} /> @@ -945,7 +1182,7 @@ const TenantsTab = memo((props) => { key={rowIndex} style={{ display: "flex", - backgroundColor: "#212121", + backgroundColor: theme.palette.platformColor, height: 30, }} > @@ -967,7 +1204,7 @@ const TenantsTab = memo((props) => { variant="text" animation="wave" sx={{ - backgroundColor: "#1a1a1a", + backgroundColor: theme.palette.loaderColor, borderRadius: "4px", }} /> @@ -978,7 +1215,7 @@ const TenantsTab = memo((props) => { ) : parentOrg?.id?.length > 0 ? ( { src={`https://flagcdn.com/w20/${parentOrgFlag}.png`} style={{ width: "30px", height: "20px", marginRight: "5px" }} /> - +
} style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} @@ -1086,7 +1323,7 @@ const TenantsTab = memo((props) => { padding: "10px", whiteSpace: "nowrap", }} - primary={index === 1 ? "Parent Organization not found or May be you are not part of parent org. Please contact support@shuffler.io." : null} + primary={index === 1 ? `Parent Organization not found or May be you are not part of parent org. Please contact ${supportEmail}` : null} colSpan={index === 0 ? 5 : undefined} /> ))} @@ -1114,17 +1351,15 @@ const TenantsTab = memo((props) => { marginTop: 20, }} > -

Sub Organizations of the Current Organization ({subOrgs.length}) -

+
{/* { }} /> */} -
+
{ overflowX: "auto", paddingBottom: 0, }}> - - - - {isCloud && ( - - )} - - - - {subOrgs.map((data, index) => { - let regiontag = "UK"; - let regionCode = "gb"; + {!suborglistOpen ? + 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"; + }} + > + setSuborglistOpen(true)} + > + Show Sub-Organizations + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + paddingLeft: 20, + display: "table-cell", + padding: "0px 8px 8px 8px", + textAlign: "center", + borderBottom: theme.palette.defaultBorder, + verticalAlign: "middle", + }} + /> + + : + + + + + {isCloud && ( + + )} + + + + {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"; + } } } - } + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; + if (index % 2 === 0) { + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; + } - return ( - - } style={{ width: 100, - minWidth: 100, - maxWidth: 100, - display: "table-cell", - padding: "8px 8px 8px 20px", - textAlign: "center", }} /> - + return ( + + } style={{ width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", }} /> + - {isCloud && ( - - {regiontag} - -
- } - style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} - /> - )} - + {isCloud && ( + + {regiontag} + +
+ } + style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} + /> + )} + - + + + + + + {selectedOrganization?.creator_org?.length > 0 ? null : - - } - style={{ display: "table-cell", verticalAlign: "middle" }} - /> - - )})} + disabled={data?.id === userdata?.active_org?.id} + onClick={() => { + setAccountDeleteButtonClicked(true); + setSelectedSuborg(data); + }} + > + Delete Org + } + + + } + style={{ display: "table-cell", verticalAlign: "middle" }} + /> + + )})} + + } +
@@ -1321,17 +1632,15 @@ const TenantsTab = memo((props) => { />
-

All Tenants -

+
{/* { style={{ borderRadius: 4, marginTop: 24, - border: "1px solid #494949", + border: theme.palette.defaultBorder, width: "100%", overflowX: "auto", paddingBottom: 0, @@ -1361,247 +1670,294 @@ const TenantsTab = memo((props) => { paddingBottom: 0, }} > - - - - {isCloud && ( - - )} - - - + {!allTenantsOpen ? + ( - - {Array(7) - .fill() - .map((_, colIndex) => ( - - - - ))} - - )) - ) : ( - 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"; + }} + > + setAllTenantsOpen(true)} + > + Show ALL your tenants + } - } - } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + paddingLeft: 20, + display: "table-cell", + padding: "0px 8px 8px 8px", + textAlign: "center", + borderBottom: theme.palette.defaultBorder, + verticalAlign: "middle", + }} + /> + + : + + + + + {isCloud && ( + + )} + + + - return ( - - - } - style={{ - width: 100, - minWidth: 100, - maxWidth: 100, - display: "table-cell", - padding: "8px 8px 8px 20px", - textAlign: "center", - }} - /> - - {isCloud ? ( - - {regiontag} + {userdata?.orgs?.length <= 0 ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(7) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ) : ( + userdata?.orgs?.length > 0 && + userdata.orgs.map((data, index) => { + let regiontag = "UK"; + let regionCode = "gb"; - -
- } - style={{ - display: "table-cell", - padding: 8, - verticalAlign: "middle", - }} - > - ) : null} - - { - handleClickChangeOrg(data?.id); - }} - > - Change Active Org - + 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"; + } + } + } + + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; + if (index % 2 === 0) { + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; } - style={{ - display: "table-cell", - padding: 8, - verticalAlign: "middle", - }} - > - - ); - }) - )} + + return ( + + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", + }} + /> + + {isCloud ? ( + + {regiontag} + + +
+ } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + ) : null} + + { + handleClickChangeOrg(data?.id); + }} + > + Change Active Org + + } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + + ); + }) + )} + }
diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index 2bb924b0..81b65b94 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useContext, memo } from "react"; import { toast } from 'react-toastify'; - +import { Context } from "../context/ContextApi.jsx"; import { FormControl, InputLabel, @@ -35,7 +35,7 @@ import { import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; const MenuProps = { @@ -75,12 +75,16 @@ const UserManagmentTab = memo((props) => { const [logsViewModal, setLogsViewModal] = React.useState(false); const [ipSelected, setIpSelected] = React.useState(""); const [userLogViewing, setUserLogViewing] = React.useState({}); + const { themeMode, supportEmail, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + useEffect(() => { if (selectedOrganization?.mfa_required !== MFARequired) { setMFARequired(selectedOrganization?.mfa_required); } }, [selectedOrganization]); + useEffect(() => { if(users?.length === 0){ getUsers(); } }, []); @@ -248,6 +252,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 +298,14 @@ const UserManagmentTab = memo((props) => { }} MenuProps={MenuProps} > + + + + + + + + {selectedOrganization.child_orgs.map((org, index) => ( -1} /> @@ -347,7 +367,7 @@ const UserManagmentTab = memo((props) => { toast("Failed to deactivate user: " + responseJson.reason); } else if (responseJson.success === false) { toast( - "Failed to deactivate user. Please contact support@shuffler.io if this persists.", + `Failed to deactivate user. Please contact ${supportEmail} if this persists.`, ); } else { toast("Changed activation for user " + data.id); @@ -586,7 +606,7 @@ const UserManagmentTab = memo((props) => { }} > - + Add user @@ -603,7 +623,7 @@ const UserManagmentTab = memo((props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -636,7 +656,7 @@ const UserManagmentTab = memo((props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.textFieldStyle.color, fontSize: "1em", }, }} @@ -666,17 +686,17 @@ const UserManagmentTab = memo((props) => {
{loginInfo} - + @@ -735,7 +754,7 @@ const UserManagmentTab = memo((props) => { }} > - + Editing {selectedUser.username} @@ -752,7 +771,7 @@ const UserManagmentTab = memo((props) => { InputProps={{ style: { height: 50, - color: "white", + color: theme.palette.textFieldStyle.color, }, }} color="primary" @@ -795,7 +814,7 @@ const UserManagmentTab = memo((props) => { InputProps={{ style: { height: 50, - color: "white", + color: theme.palette.textFieldStyle.color, }, }} color="primary" @@ -829,9 +848,10 @@ const UserManagmentTab = memo((props) => { backgroundColor: theme.palette.inputColor, }} /> -
- +
{isCloud && userdata.support && selectedUser.id !== userdata.id ? (
- + {parsedTitle}
diff --git a/frontend/src/components/WorkflowValidationTimeline.jsx b/frontend/src/components/WorkflowValidationTimeline.jsx index 6df160c8..2d724cfc 100644 --- a/frontend/src/components/WorkflowValidationTimeline.jsx +++ b/frontend/src/components/WorkflowValidationTimeline.jsx @@ -398,8 +398,9 @@ const WorkflowValidationTimeline = (props) => { nodecolor = green branchcolor = green } else { - nodecolor = grey - branchcolor = grey + // Green by default + nodecolor = green + branchcolor = green } } else { nodecolor = green @@ -561,8 +562,6 @@ const WorkflowValidationTimeline = (props) => { nodecolor = green branchcolor = green } - - var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3 if (nodecolor === green) { @@ -676,7 +675,6 @@ const WorkflowValidationTimeline = (props) => { {founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
} placement="top"> - { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/ssoTab.jsx b/frontend/src/components/ssoTab.jsx index 8f39a296..e220ea6f 100644 --- a/frontend/src/components/ssoTab.jsx +++ b/frontend/src/components/ssoTab.jsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useContext } from "react"; import React from "react"; import { Typography, @@ -7,16 +7,14 @@ import { Tooltip, TextField, Grid, - Skeleton, - Dialog, - DialogTitle, - DialogContent, - Box, + Checkbox } from "@mui/material"; import { makeStyles } from "@mui/styles"; import { Link } from "react-router-dom"; import theme from "../theme.jsx"; import { toast } from "react-toastify"; +import { Context } from "../context/ContextApi.jsx"; +import { getTheme } from "../theme.jsx"; const useStyles = makeStyles({ notchedOutline: { @@ -30,6 +28,16 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle const classes = useStyles(); const [show2faSetup, setShow2faSetup] = React.useState(false); const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision) + const [roleRequired, setRoleRequired] = React.useState(selectedOrganization?.sso_config?.role_required || false); + const [showOpenIdCred, setShowOpenIdCred] = React.useState(false); + const [showSamlCred, setShowSamlCred] = React.useState(false); + const [skipSSOForAdmin, setSkipSSOForAdmin] = React.useState( + selectedOrganization?.sso_config === undefined + ? false + : selectedOrganization?.sso_config?.skip_sso_for_admins === undefined + ? false + : selectedOrganization?.sso_config?.skip_sso_for_admins + ); const [ssoEntrypoint, setSsoEntrypoint] = React.useState( selectedOrganization.sso_config === undefined ? "" @@ -86,6 +94,9 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle : selectedOrganization.sso_config.openid_token ) + const { themeMode, supportEmail, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + useEffect(()=>{ if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) { @@ -118,12 +129,21 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle if (autoPrivision !== selectedOrganization?.sso_config?.auto_provision) { setAutoProvision(selectedOrganization?.sso_config?.auto_provision) } + + if (roleRequired !== selectedOrganization?.sso_config?.role_required) { + setRoleRequired(selectedOrganization?.sso_config?.role_required) + } + + if (skipSSOForAdmin !== selectedOrganization?.sso_config?.skip_sso_for_admins) { + setSkipSSOForAdmin(selectedOrganization?.sso_config?.skip_sso_for_admins) + } + },[selectedOrganization]) const orgSaveButton = (
- OpenID connect - + OpenID connect + Configure and Authorize SAML / SSO or OpenID connect. {" "} Learn more - - IdP URL for Shuffle OpenID: {`${globalUrl}/api/v1/login_openid`} - + + IdP URL for Shuffle OpenID: {`${globalUrl}/api/v1/login_openid`} + +
+ + Show OpenID Credentials + + setShowOpenIdCred(e.target.checked)} + name="showOpenIdCred" + color="primary" + /> +
- Client ID + Client ID { - setOpenidClientId(e.target.value); - }} + value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""} + onChange={(e) => setOpenidClientId(e.target.value)} + onFocus={(e) => setShowOpenIdCred(true)} + onBlur={(e) => setShowOpenIdCred(false)} InputProps={{ classes: { - notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", - fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", - fontWeight: 400, - fontSize: 16, - borderRadius: 4, + color: theme.palette.textFieldStyle.color, + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: theme.palette.textFieldStyle.borderRadius, }, }} - /> + /> - Client Secret + Client Secret 0 ? "•".repeat(50) : ""} onChange={(e) => { setOpenidClientSecret(e.target.value); - }} + }} + onFocus={(e) => setShowOpenIdCred(true)} + onBlur={(e) => setShowOpenIdCred(false)} InputProps={{ classes: { notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16, - borderRadius: 4, + borderRadius: theme.palette.textFieldStyle.borderRadius, }, }} /> @@ -475,33 +598,35 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle - Authorization URL + Authorization URL 0 ? "•".repeat(50) : ""} onChange={(e) => { setOpenidAuthorization(e.target.value) - }} + }} + onFocus={(e) => setShowOpenIdCred(true)} + onBlur={(e) => setShowOpenIdCred(false)} InputProps={{ classes: { notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16, @@ -513,33 +638,35 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle - Token URL + Token URL 0 ? "•".repeat(50) : ""} onChange={(e) => { setOpenidToken(e.target.value) - }} + }} + onFocus={(e) => setShowOpenIdCred(true)} + onBlur={(e) => setShowOpenIdCred(false)} InputProps={{ classes: { notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", + color:theme.palette.textFieldStyle.color, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16, @@ -554,40 +681,61 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle {/**/} {/*isCloud ? null : */} - SAML SSO (v1.1) - - IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} - + SAML SSO (v1.1) + + IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} + +
+ + Show SAML Credentials + + setShowSamlCred(e.target.checked)} + name="showSamlCred" + color="primary" + /> +
- SSO Entrypoint (IdP) + SSO Entrypoint (IdP) 0 ? "•".repeat(50) : ""} onChange={(e) => { setSsoEntrypoint(e.target.value); - }} + }} + onFocus={(e) => setShowSamlCred(true)} + onBlur={(e) => setShowSamlCred(false)} InputProps={{ classes: { notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16, @@ -599,24 +747,26 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle - SSO Certificate (X509) + SSO Certificate (X509) 0 ? "•".repeat(50) : ""} + onFocus={(e) => setShowSamlCred(true)} + onBlur={(e) => setShowSamlCred(false)} onChange={(e) => { setSsoCertificate(e.target.value); }} @@ -625,7 +775,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle notchedOutline: isEditOrgTab ? null : classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16, @@ -646,4 +796,4 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle ) } -export default SSOTab \ No newline at end of file +export default SSOTab diff --git a/frontend/src/context/ContextApi.jsx b/frontend/src/context/ContextApi.jsx index 3932dfd0..d344e3ed 100644 --- a/frontend/src/context/ContextApi.jsx +++ b/frontend/src/context/ContextApi.jsx @@ -1,14 +1,26 @@ -import { createContext, useState, useEffect } from 'react'; +import React, { createContext, useState, useEffect } from 'react'; +import useMediaQuery from '@mui/material/useMediaQuery'; + export const Context = createContext(); -export const AppContext =(props) => { +export const AppContext = (props) => { + const { serverside } = props - const currentLocation = window?.location?.pathname; + const currentLocation = serverside === true ? "" : window?.location?.pathname; // Left side bar global states const [searchBarModalOpen, setSearchBarModalOpen] = useState(false); + const [isDocSearchModalOpen, setIsDocSearchModalOpen] = useState(false); const [leftSideBarOpenByClick, setLeftSideBarOpenByClick] = useState(currentLocation?.includes('/workflows/') ? false : true) - const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const [windowWidth, setWindowWidth] = useState(serverside === true ? 100 : window.innerWidth); + const [brandColor, setBrandColor] = useState(() => localStorage.getItem("brandColor") || "#ff8544"); + const [brandName, setBrandName] = useState(()=> localStorage.getItem("brandName") || "Shuffle"); + + const [themeMode, setThemeMode] = useState( + () => localStorage.getItem("theme") || "dark" + ); + const [supportEmail, setSupportEmail] = useState("support@shuffler.io"); + const [logoutUrl, setLogoutUrl] = useState(""); useEffect(() => { if (currentLocation?.includes('/workflows/') && leftSideBarOpenByClick === true) { @@ -18,6 +30,10 @@ export const AppContext =(props) => { //Calculate window width useEffect(() => { + if (serverside === true) { + return + } + const handleResize = () => { setWindowWidth(window?.innerWidth); }; @@ -29,14 +45,78 @@ export const AppContext =(props) => { }; }, []); + const handleThemeChange = (theme) => { + if (!theme || theme === "null" || theme === "undefined") { + localStorage.setItem("theme", "dark"); + setThemeMode("dark"); + return; + } + + const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const applySystemTheme = () => { + const isDark = darkMediaQuery.matches; + setThemeMode(isDark ? "dark" : "light"); + localStorage.setItem("theme", isDark ? "dark" : "light"); + }; + + if (theme === "system") { + applySystemTheme(); + darkMediaQuery.addEventListener("change", applySystemTheme); + return () => { + darkMediaQuery.removeEventListener("change", applySystemTheme); + }; + } else { + setThemeMode(theme); + localStorage.setItem("theme", theme); + } + }; + + + useEffect(() => { + if (serverside === true) return; + + const theme = localStorage.getItem("theme"); + + if (!theme || theme === "null" || theme === "undefined") { + localStorage.setItem("theme", "dark"); + setThemeMode("dark"); + return; + } + + let cleanup; + if (theme === "system") { + cleanup = handleThemeChange("system"); + } else { + handleThemeChange(theme); + } + + return () => { + if (cleanup) cleanup(); + }; + }, []); + return ( {props.children} diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index d7a6b68e..79d35e35 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -1,586 +1,626 @@ -const data = [ - { - selector: "node", - css: { - label: function(element) { - var elementname = element.data("label") - if (elementname === null || elementname === undefined) { - return "" - } - - elementname = elementname.replaceAll("_", " ", -1) - elementname = elementname.charAt(0).toUpperCase() + elementname.slice(1) - return elementname - }, - "text-valign": "center", - "text-margin-x": function(element) { - // Attempt at bottom-positioning - // Required text-valign: bottom - // FIXME: Disabled for now. - return "15px" - - - - const name = element.data("label") - console.log("Name: ", name) - if (name === null || name === undefined || name == "" || document=== undefined || document === null) { - return "0px" - } - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d') - - context.font = '18px Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif' - - const textWidth = context.measureText(name).width - return textWidth + "px" - //return -1*(textWidth) + "px" - }, - - "font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif", - "font-weight": "lighter", - "font-size": "18px", - "margin-right": "10px", - width: "80px", - height: "80px", - color: "white", - padding: "10px", - margin: "5px", - "border-width": "1px", - "z-index": 5001, +export default function defaultCytoscapeStyle(theme) { + return [ + { + selector: "node", + css: { + label: function(element) { + var elementname = element.data("label") + if (elementname === null || elementname === undefined) { + return "" + } + + elementname = elementname.replaceAll("_", " ", -1) + elementname = elementname.charAt(0).toUpperCase() + elementname.slice(1) + return elementname + }, + "text-valign": "center", + "text-margin-x": function(element) { + // Attempt at bottom-positioning + // Required text-valign: bottom + // FIXME: Disabled for now. + return "15px" + + + + const name = element.data("label") + console.log("Name: ", name) + if (name === null || name === undefined || name == "" || document=== undefined || document === null) { + return "0px" + } + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d') + + context.font = '18px Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif' + + const textWidth = context.measureText(name).width + return textWidth + "px" + //return -1*(textWidth) + "px" + }, + + "font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif", + "font-weight": "lighter", + "font-size": "18px", + "margin-right": "10px", + width: "80px", + height: "80px", + color: theme.palette.text.primary || "white", + padding: "10px", + margin: "5px", + "border-width": "1px", + "z-index": 5001, + }, }, - }, - { - selector: "edge", - css: { - "target-arrow-shape": "triangle", - "target-arrow-color": "grey", - "curve-style": "unbundled-bezier", - label: "data(label)", - "text-margin-y": "-15px", - width: "2px", - color: "white", - "line-fill": "linear-gradient", - "line-gradient-stop-positions": ["0.0", "100"], - "line-gradient-stop-colors": ["grey", "grey"], - "z-index": 5001, + { + selector: "edge", + css: { + "target-arrow-shape": "triangle", + "target-arrow-color": "grey", + "curve-style": "unbundled-bezier", + label: "data(label)", + "text-margin-y": "-15px", + width: "2px", + color: theme.palette.text.primary || "white", + "line-fill": "linear-gradient", + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["grey", "grey"], + "z-index": 5001, + }, }, - }, - { - selector: `node[buttonType="ACTIONSUGGESTION"]`, - css: { - label: "data(label)", - shape: "roundrectangle", - "height": "18px", - "width": "145px", - "background-color": "#212121", - "border-color": "#81c784", - "z-index": 10000, - "border-radius": "10px", - "text-margin-x": "0px", - }, - }, - { - selector: `node[type="ACTION"]`, - css: { - shape: "roundrectangle", - "background-color": "#213243", - "border-color": "#81c784", - "background-width": "100%", - "background-height": "100%", - "border-radius": "5px", - "z-index": 5001, - }, - }, - { - selector: `node[type="COMMENT"]`, - css: { - label: function(element) { - return element.data("label") - }, - shape: "roundrectangle", - color: "data(color)", - width: "data(width)", - height: "data(height)", - padding: "0px", - margin: "0px", - "background-color": "data(backgroundcolor)", - "background-image": "data(backgroundimage)", - "border-color": "#ffffff", + { + selector: `node[buttonType="ACTIONSUGGESTION"]`, + css: { + label: "data(label)", + shape: "roundrectangle", + "height": "18px", + "width": "145px", + "background-color": "#212121", + "border-color": "#81c784", + "z-index": 10000, + "border-radius": "10px", "text-margin-x": "0px", - "z-index": 4999, - "border-radius": "5px", + }, + }, + { + selector: `node[type="ACTION"]`, + css: { + shape: "roundrectangle", + "background-color": "#213243", + "border-color": "#81c784", + "background-width": "100%", + "background-height": "100%", + "border-radius": "5px", + "z-index": 5001, + }, + }, + { + selector: `node[type="COMMENT"]`, + css: { + label: function (element) { + return element.data("label") + }, + shape: "roundrectangle", + color: "data(color)", + width: "data(width)", + height: "data(height)", + padding: "5px", + margin: "0px", + "background-color": "data(backgroundcolor)", + "background-image": "data(backgroundimage)", + "border-color": "#ffffff", + "text-margin-x": "data(textMarginX)", + "text-margin-y": "data(textMarginY)", + "z-index": 4999, + "border-radius": "5px", + "background-opacity": "0.5", + "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": theme.palette.text.primary || "white", + "z-index": 5002, + "overlay-opacity": 0, + "cursor": "nwse-resize", + "opacity": 0, + "pointer-events": "auto", + }, + }, + { + selector: `node[app_name="Integration Framework"]`, + css: { + width: "60px", + height: "60px", + "z-index": 5000, + + //'border-width': 3, + //'border-color': 'transparent', + //'border-style': 'solid', + //'border-gradient': 'linear-gradient(to right, #FF0000, #FF7F00, #FFFF00, #00FF00, #0000FF, #4B0082, #8A2BE2)' + }, + }, + { + selector: `node[example="noapp"]`, + css: { + // Make background image padding on the left side 20px + "background-width": "100%", + "background-height": "100%", + + "background-color": "data(iconBackground)", + "background-fill": "data(fillstyle)", + "background-gradient-direction": "to-bottom-right", + "background-gradient-stop-colors": "data(fillGradient)", + // Change transparency of background + "background-opacity": "0.3", + }, + }, + { + selector: `node[app_name="Shuffle Tools"], node[app_name="email"], node[app_name="http"]`, + css: { + width: "35px", + height: "35px", + "z-index": 5000, + "font-size": "0px", + "background-width": "75%", + "background-height": "75%", + "background-color": "data(iconBackground)", + "background-fill": "data(fillstyle)", + "background-gradient-direction": "to-right", + "background-gradient-stop-colors": "data(fillGradient)", + }, + }, + { + selector: `node[app_name="Testing"]`, + css: { + width: "30px", + height: "30px", + "z-index": 5000, + "font-size": "0px", + }, + }, + { + selector: `node[?small_image]`, + css: { + "background-image": "data(small_image)", + "text-halign": "right", + }, + }, + { + selector: `node[?large_image]`, + css: { + "background-image": "data(large_image)", + "text-halign": "right", + }, + }, + { + selector: `node[type="CONDITION"]`, + css: { + shape: "diamond", + "border-color": "##FFEB3B", + padding: "30px", + }, + }, + { + selector: `node[type="eventAction"]`, + css: { + "background-color": "#edbd21", + }, + }, + { + selector: `node[type="TRIGGER"]`, + css: { + shape: "round-octagon", + "border-radius": "5px", + "border-color": "orange", + "background-color": "#213243", + "background-width": "100px", + "background-height": "100px", + }, + }, + { + selector: `node[status="running"]`, + css: { + "border-color": "#81c784", + }, + }, + { + selector: `node[status="stopped"]`, + css: { + "border-color": "orange", + }, + }, + { + selector: 'node[type="mq"]', + css: { + "background-color": "#edbd21", + }, + }, + { + selector: "node[?isButton]", + css: { + shape: "ellipse", + width: "15px", + height: "15px", + "z-index": "5002", + "font-size": "0px", + border: "1px solid rgba(255,255,255,0.9)", + "background-image": "data(icon)", + "background-color": "data(iconBackground)", + }, + }, + { + selector: "node[?isSuggestion]", + css: { + shape: "roundrectangle", + width: "30px", + height: "30px", + "z-index": "5002", + filter: "grayscale(100%)", + border: "1px solid rgba(255,255,255,0.9)", + "background-image": "data(large_image)", + "background-fit": "cover", + "font-size": "20px", + label: "data(label_replaced)", + }, + }, + { + selector: "node[?canConnect]", + css: { + "border-color": "#f86a3e", + "border-width": "10px", + "z-index": "5002", + "background-color": "#f86a3e", + }, + }, + { + selector: "node[?isDescriptor]", + css: { + shape: "ellipse", + "border-color": "#80deea", + width: "5px", + height: "5px", + "z-index": "5002", + "font-size": "10px", + "text-valign": "center", + "text-halign": "center", + border: "1px solid black", + "margin-right": "0px", + "text-margin-x": "0px", + "background-color": "data(imageColor)", + "background-image": "data(image)", + label: "data(label)", + }, + }, + { + selector: "node[?isStartNode]", + css: { + shape: function(element) { + return "ellipse" + }, + "border-color": "#80deea", + width: "80px", + height: "80px", + "font-size": "18px", + "background-width": "100%", + "background-height": "100%", + }, + }, + { + selector: `node[app_id="shuffle_agent"]`, + css: { + "shape": function(element) { + return "roundrectangle" + }, + "height": "74px", + "width": "222px", + "background-image": "data(large_image)", + "label": function(element) { + var elementname = element.data("label") + if (elementname === null || elementname === undefined) { + return "" + } + + elementname = elementname.replaceAll("_", " ", -1) + + if (elementname.length > 15) { + elementname = elementname.substring(0, 15) + ".." + } + + return elementname + }, + "background-width": "65px", + "background-height": "65px", + "background-position-x": "20px", + "background-repeat": "no-repeat", "background-opacity": "0.5", - "text-wrap": "wrap", - }, - }, - { - selector: `node[app_name="Integration Framework"]`, - css: { - width: "60px", - height: "60px", - "z-index": 5000, - - //'border-width': 3, - //'border-color': 'transparent', - //'border-style': 'solid', - //'border-gradient': 'linear-gradient(to right, #FF0000, #FF7F00, #FFFF00, #00FF00, #0000FF, #4B0082, #8A2BE2)' - }, - }, - { - selector: `node[example="noapp"]`, - css: { - // Make background image padding on the left side 20px - "background-width": "100%", - "background-height": "100%", - - "background-color": "data(iconBackground)", - "background-fill": "data(fillstyle)", - "background-gradient-direction": "to-bottom-right", - "background-gradient-stop-colors": "data(fillGradient)", - // Change transparency of background - "background-opacity": "0.3", - }, - }, - { - selector: `node[app_name="Shuffle Tools"], node[app_name="email"], node[app_name="http"]`, - css: { - width: "35px", - height: "35px", - "z-index": 5000, - "font-size": "0px", - "background-width": "75%", - "background-height": "75%", - "background-color": "data(iconBackground)", - "background-fill": "data(fillstyle)", - "background-gradient-direction": "to-right", - "background-gradient-stop-colors": "data(fillGradient)", - }, - }, - { - selector: `node[app_id="shuffle_agent"]`, - css: { - "height": "74px", - "width": "222px", - "background-image": "data(large_image)", - "label": function(element) { - var elementname = element.data("label") - if (elementname === null || elementname === undefined) { - return "" - } - - if (elementname.length > 15) { - elementname = elementname.substring(0, 15) + ".." - } - - return elementname - }, - "background-width": "65px", - "background-height": "65px", - "background-position-x": "20px", - //"background-position-x": "center", // Crashes - "background-repeat": "no-repeat", - - "font-size": "14px", - "text-halign": "center", - "text-valign": "center", - "text-margin-x": "-140px", - "text-margin-y": "0px", - - }, - }, - { - selector: `node[app_name="Testing"]`, - css: { - width: "30px", - height: "30px", - "z-index": 5000, - "font-size": "0px", - }, - }, - { - selector: `node[?small_image]`, - css: { - "background-image": "data(small_image)", - "text-halign": "right", - }, - }, - { - selector: `node[?large_image]`, - css: { - "background-image": "data(large_image)", - "text-halign": "right", - }, - }, - { - selector: `node[type="CONDITION"]`, - css: { - shape: "diamond", - "border-color": "##FFEB3B", - padding: "30px", - }, - }, - { - selector: `node[type="eventAction"]`, - css: { - "background-color": "#edbd21", - }, - }, - { - selector: `node[type="TRIGGER"]`, - css: { - shape: "round-octagon", - "border-radius": "5px", - "border-color": "orange", - "background-color": "#213243", - "background-width": "100px", - "background-height": "100px", - }, - }, - { - selector: `node[status="running"]`, - css: { - "border-color": "#81c784", - }, - }, - { - selector: `node[status="stopped"]`, - css: { - "border-color": "orange", - }, - }, - { - selector: 'node[type="mq"]', - css: { - "background-color": "#edbd21", - }, - }, - { - selector: "node[?isButton]", - css: { - shape: "ellipse", - width: "15px", - height: "15px", - "z-index": "5002", - "font-size": "0px", - border: "1px solid rgba(255,255,255,0.9)", - "background-image": "data(icon)", - "background-color": "data(iconBackground)", - }, - }, - { - selector: "node[?isSuggestion]", - css: { - shape: "roundrectangle", - width: "30px", - height: "30px", - "z-index": "5002", - filter: "grayscale(100%)", - border: "1px solid rgba(255,255,255,0.9)", - "background-image": "data(large_image)", - "background-fit": "cover", - "font-size": "20px", - label: "data(label_replaced)", - }, - }, - { - selector: "node[?canConnect]", - css: { - "border-color": "#f86a3e", - "border-width": "10px", - "z-index": "5002", - "background-color": "#f86a3e", - }, - }, - { - selector: "node[?isDescriptor]", - css: { - shape: "ellipse", - "border-color": "#80deea", - width: "5px", - height: "5px", - "z-index": "5002", - "font-size": "10px", - "text-valign": "center", + + "font-size": "14px", "text-halign": "center", - border: "1px solid black", - "margin-right": "0px", - "text-margin-x": "0px", - "background-color": "data(imageColor)", - "background-image": "data(image)", - label: "data(label)", + "text-valign": "center", + "text-margin-x": "20px", + "text-margin-y": "0px", + + }, }, - }, - { - selector: "node[?isStartNode]", - css: { - shape: function(element) { - return "ellipse" - }, - "border-color": "#80deea", - width: "80px", - height: "80px", - "font-size": "18px", - "background-width": "100%", - "background-height": "100%", + { + selector: "node[!is_valid]", + css: { + "border-color": "#f53434", + "border-width": "5px", + }, }, - }, - { - selector: "node[!is_valid]", - css: { - "border-color": "#f53434", - "border-width": "5px", + { + selector: ":selected", + css: { + "background-color": "#77b0d0", + "border-color": "#77b0d0", + "border-width": "20px", + }, }, - }, - { - selector: ":selected", - css: { - "background-color": "#77b0d0", - "border-color": "#77b0d0", - "border-width": "20px", + { + selector: ".skipped-highlight", + css: { + "background-color": "grey", + "border-color": "grey", + "border-width": "8px", + "transition-property": "background-color", + "transition-duration": "0.5s", + }, }, - }, - { - selector: ".skipped-highlight", - css: { - "background-color": "grey", - "border-color": "grey", - "border-width": "8px", - "transition-property": "background-color", - "transition-duration": "0.5s", + { + selector: ".success-highlight", + css: { + "background-color": "#41dcab", + "border-color": "#41dcab", + "border-width": "5px", + "transition-property": "background-color", + "transition-duration": "0.5s", + }, }, - }, - { - selector: ".success-highlight", - css: { - "background-color": "#41dcab", - "border-color": "#41dcab", - "border-width": "5px", - "transition-property": "background-color", - "transition-duration": "0.5s", + { + selector: ".hover-highlight", + css: { + "background-color": "#5f9265", + "border-color": "#5f9265", + "border-width": "5px", + "transition-property": "background-color", + "transition-duration": "0.5s", + }, }, - }, - { - selector: ".hover-highlight", - css: { - "background-color": "#5f9265", - "border-color": "#5f9265", - "border-width": "5px", - "transition-property": "background-color", - "transition-duration": "0.5s", + { + selector: ".failure-highlight", + css: { + "background-color": "#8e3530", + "border-color": "#8e3530", + "border-width": "5px", + "transition-property": "background-color", + "transition-duration": "0.5s", + }, }, - }, - { - selector: ".failure-highlight", - css: { - "background-color": "#8e3530", - "border-color": "#8e3530", - "border-width": "5px", - "transition-property": "background-color", - "transition-duration": "0.5s", + { + selector: ".not-executing-highlight", + css: { + "background-color": "grey", + "border-color": "grey", + "border-width": "5px", + "transition-property": "#ffef47", + "transition-duration": "0.25s", + }, }, - }, - { - selector: ".not-executing-highlight", - css: { - "background-color": "grey", - "border-color": "grey", - "border-width": "5px", - "transition-property": "#ffef47", - "transition-duration": "0.25s", + { + selector: ".executing-highlight", + css: { + //"background-color": "#ffef47", + "border-color": "#ffef47", + "border-width": "8px", + "transition-property": "border-width", + "transition-duration": "0.25s", + }, }, - }, - { - selector: ".executing-highlight", - css: { - "background-color": "#ffef47", - "border-color": "#ffef47", - "border-width": "8px", - "transition-property": "border-width", - "transition-duration": "0.25s", + { + selector: ".awaiting-data-highlight", + css: { + "background-color": "#f4ad42", + "border-color": "#f4ad42", + "border-width": "5px", + "transition-property": "border-color", + "transition-duration": "0.5s", + }, }, - }, - { - selector: ".awaiting-data-highlight", - css: { - "background-color": "#f4ad42", - "border-color": "#f4ad42", - "border-width": "5px", - "transition-property": "border-color", - "transition-duration": "0.5s", + { + selector: ".shuffle-hover-highlight", + css: { + "background-color": "#f85a3e", + "border-color": "#f85a3e", + "border-width": "7px", + "transition-property": "border-width", + "transition-duration": "0.25s", + label: "data(label)", + "font-size": "18px", + color: theme.palette.text.primary || "white", + }, }, - }, - { - selector: ".shuffle-hover-highlight", - css: { + { + selector: "$node > node", + css: { + "padding-top": "10px", + "padding-left": "10px", + "padding-bottom": "10px", + "padding-right": "10px", + }, + }, + { + selector: "edge.executing-highlight", + css: { + width: "5px", + "target-arrow-color": "#ffef47", + "line-color": "#ffef47", + "transition-property": "line-color, width", + "transition-duration": "0.25s", + }, + }, + { + selector: `edge[?decorator]`, + css: { + width: "1px", + "line-style": "dashed", + "line-fill": "linear-gradient", + "target-arrow-color": "#555555", + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["#555555", "#555555"], + }, + }, + { + selector: "edge.success-highlight", + css: { + width: "3px", + "target-arrow-color": "#41dcab", + "line-color": "#41dcab", + "transition-property": "line-color, width", + "transition-duration": "0.5s", + "line-fill": "linear-gradient", + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["#41dcab", "#41dcab"], + }, + }, + { + selector: ".eh-handle", + style: { + "background-color": "#337ab7", + width: "1px", + height: "1px", + shape: "circle", + "border-width": "1px", + "border-color": "black", + }, + }, + { + selector: ".eh-source", + style: { + "border-width": "3", + "border-color": "#337ab7", + }, + }, + { + selector: ".eh-target", + style: { + "border-width": "3", + "border-color": "#337ab7", + }, + }, + { + selector: ".eh-preview, .eh-ghost-edge", + style: { + "background-color": "#337ab7", + "line-color": "#337ab7", + "target-arrow-color": "#337ab7", + "source-arrow-color": "#337ab7", + }, + }, + { + selector: "edge:selected", + css: { + "target-arrow-color": "#f85a3e", + }, + }, + { + selector: `edge[?source_workflow]`, + css: { + "background-opacity": "1", + "font-size": "0px", + }, + }, + { + selector: `node[?source_workflow]`, + css: { + "background-opacity": "0", + "font-size": "0px", + }, + }, + { + selector: "node:selected", + css: { + "border-color": "#f86a3e", + "border-width": "7px", + }, + }, + { + selector: `node[buttonType="condition-drag"]`, + css: { + "width": "5px", + "height": "5px", "background-color": "#f85a3e", - "border-color": "#f85a3e", - "border-width": "7px", - "transition-property": "border-width", - "transition-duration": "0.25s", - label: "data(label)", - "font-size": "18px", - color: "white", + }, }, - }, - { - selector: "$node > node", - css: { - "padding-top": "10px", - "padding-left": "10px", - "padding-bottom": "10px", - "padding-right": "10px", + { + selector: `node[name="switch"]`, + css: { + label: function(element) { + // Load from the actual element + var nodeheight = 400 + var conditions = [{ + "name": "Condition 1", + "check": "X equals Y", + }, + { + "name": "Condition 2", + "check": "X2 equals Y2", + }, + { + "name": "Condition 3", + "check": "X3 equals Y3", + }] + + conditions.push({ + "name": "Else", + "check": "If all else fails", + }) + + const newlines = nodeheight / conditions.length + console.log("Newlines: ", newlines) + + const label = conditions.map((condition) => { + return `${condition.name}\n\n\n` + }).join("\n") + + return label + }, + color: theme.palette.text.primary || "white", + "border-color": "#f85a3e", + "background-color": "#1f1f1f", + "font-size": "19px", + "text-margin-x": "-110px", + "text-wrap": "wrap", + shape: "roundrectangle", + width: "100", + height: "300", + + }, }, - }, - { - selector: "edge.executing-highlight", - css: { - width: "5px", - "target-arrow-color": "#ffef47", - "line-color": "#ffef47", - "transition-property": "line-color, width", - "transition-duration": "0.25s", - }, - }, - { - selector: `edge[?decorator]`, - css: { - width: "1px", - "line-style": "dashed", - "line-fill": "linear-gradient", - "target-arrow-color": "#555555", - "line-gradient-stop-positions": ["0.0", "100"], - "line-gradient-stop-colors": ["#555555", "#555555"], - }, - }, - { - selector: "edge.success-highlight", - css: { - width: "3px", - "target-arrow-color": "#41dcab", - "line-color": "#41dcab", - "transition-property": "line-color, width", - "transition-duration": "0.5s", - "line-fill": "linear-gradient", - "line-gradient-stop-positions": ["0.0", "100"], - "line-gradient-stop-colors": ["#41dcab", "#41dcab"], - }, - }, - { - selector: ".eh-handle", - style: { - "background-color": "#337ab7", - width: "1px", - height: "1px", - shape: "circle", - "border-width": "1px", - "border-color": "black", - }, - }, - { - selector: ".eh-source", - style: { - "border-width": "3", - "border-color": "#337ab7", - }, - }, - { - selector: ".eh-target", - style: { - "border-width": "3", - "border-color": "#337ab7", - }, - }, - { - selector: ".eh-preview, .eh-ghost-edge", - style: { - "background-color": "#337ab7", - "line-color": "#337ab7", - "target-arrow-color": "#337ab7", - "source-arrow-color": "#337ab7", - }, - }, - { - selector: "edge:selected", - css: { - "target-arrow-color": "#f85a3e", - }, - }, - { - selector: `edge[?source_workflow]`, - css: { - "background-opacity": "1", - "font-size": "0px", - }, - }, - { - selector: `node[?source_workflow]`, - css: { - "background-opacity": "0", - "font-size": "0px", - }, - }, - { - selector: "node:selected", - css: { - "border-color": "#f86a3e", - "border-width": "7px", - }, - }, - { - selector: `node[buttonType="condition-drag"]`, - css: { - "width": "5px", - "height": "5px", - "background-color": "#f85a3e", - }, - }, - { - selector: `node[name="switch"]`, - css: { - label: function(element) { - // Load from the actual element - var nodeheight = 400 - var conditions = [{ - "name": "Condition 1", - "check": "X equals Y", - }, - { - "name": "Condition 2", - "check": "X2 equals Y2", - }, - { - "name": "Condition 3", - "check": "X3 equals Y3", - }] - - conditions.push({ - "name": "Else", - "check": "If all else fails", - }) - - const newlines = nodeheight / conditions.length - console.log("Newlines: ", newlines) - - const label = conditions.map((condition) => { - return `${condition.name}\n\n\n` - }).join("\n") - - return label - }, - color: "white", - "border-color": "#f85a3e", - "background-color": "#1f1f1f", - "font-size": "19px", - "text-margin-x": "-110px", - "text-wrap": "wrap", - shape: "roundrectangle", - width: "100", - height: "300", - - }, - }, -]; + ]; +} //{ // selector: 'edge[?hasErrors]', @@ -593,5 +633,3 @@ const data = [ // "line-gradient-stop-colors": ["#991818", "#991818"], // }, //}, - -export default data; diff --git a/frontend/src/index.js b/frontend/src/index.js index d005b55a..20b3ae48 100755 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,7 @@ import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; +import { AppContext } from "./context/ContextApi"; //import "./index.css"; //import reportWebVitals from "./reportWebVitals"; @@ -9,7 +10,9 @@ const rootElement = document.getElementById("root"); const root = createRoot(rootElement); root.render( - + + + ); diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx index ae5fcbf9..4835e4f1 100644 --- a/frontend/src/theme.jsx +++ b/frontend/src/theme.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { createTheme, adaptV4Theme } from "@mui/material/styles"; const theme = createTheme(adaptV4Theme({ @@ -27,7 +26,7 @@ const theme = createTheme(adaptV4Theme({ distributionColor: "#40E0D0", green: "#5cc879", - borderRadius: 10, + borderRadius: 8, defaultBorder: "1px solid rgba(255,255,255,0.3)", //jsonTheme: "brewer", @@ -81,7 +80,7 @@ const theme = createTheme(adaptV4Theme({ singulBlackWhite: "/images/singul_black_white.png", }, typography: { - fontFamily: `"Roboto", "Helvetica", "Arial", "inter", sans-serif`, + fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, useNextVariants: true, fontWeightLight: 300, fontWeightRegular: 400, @@ -112,39 +111,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'); } `, }, @@ -153,3 +152,302 @@ const theme = createTheme(adaptV4Theme({ })); export default theme; + +export const getTheme = (themeMode, brandColor) => + createTheme({ + palette: { + mode: themeMode, + main: brandColor || "#FF8544", + primary: { + main: brandColor || "#FF8544", + contrastText: "#ffffff", + }, + secondary: { + main: "rgba(255,255,255,0.7)", + contrastText:"#000000", + }, + text: { + primary: themeMode === "dark" ? "#ffffff" : "#1A1A1A", + secondary: themeMode === "dark" ? "#9E9E9E" : "#616161", + }, + type: themeMode, + inputColor: themeMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)", + textColor: themeMode === "dark" ? "#F1F1F1" : "#1A1A1A", + textPrimary: themeMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)", + surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF", + platformColor: themeMode === "dark" ? "#212121" : "#ffffff", + backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1", + distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080", + cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea", + cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0", + hoverColor: themeMode === "dark" ? "#323232" : "#D6D6D6", + usecaseCardColor: themeMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)", + usecaseCardHoverColor: themeMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)", + usecaseDialogFieldColor: themeMode === "dark" ? "#2B2B2B" : "#F5F5F5", + accentColor: themeMode === "dark" ? "#ff8544" : "#ff8544", + green: themeMode === "dark" ? "#5cc879" : "#008000", + defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC', + linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor, + + borderRadius: 10, + loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0", + jsonIconStyle: "round", + jsonTheme: themeMode === "dark" ? "summerfruit" : { + base00: "#ffffff", // background + base01: "#f0f0f0", // very light grey + base02: "#f5f5f5", // light grey + base03: "#999999", // dim text + base04: "#444444", // bold keys + base05: "#333333", // normal text + base06: "#1a1a1a", // darker text + base07: "#000000", // black + base08: "#f14c4c", // red + base09: "#f58c1f", // orange + base0A: "#f2c032", // yellow + base0B: "#51975d", // green + base0C: "#2aa198", // teal + base0D: "#007acc", // blue (keys!) + base0E: "#c586c0", // purple + base0F: "#d16969", // brown + }, + jsonCollapseStringsAfterLength: 100, + drawer: { + backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9" + }, + reactJsonStyle: { + padding: 5, + width: "98%", + borderRadius: 5, + border: themeMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)", + backgroundColor: themeMode === "dark" + ? "#1A1A1A" + : "#f1f1f1", + color: themeMode === "dark" + ? "#F1F1F1" + : "#1A1A1A", + overflowX: "auto", + }, + textFieldStyle: { + backgroundColor: themeMode === "dark" ? "#212121" : "#FFFFFF", + color: themeMode === "dark" ? "#ffffff" : "#000000", + borderRadius: "5px", + height: 40, + border: themeMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0", + }, + DialogStyle: { + backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", + borderRadius: 2, + boxShadow: themeMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)", + border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", + }, + innerTextfieldStyle: { + height: 40, + fontSize: 16, + backgroundColor: themeMode === "dark" ? "#212121" : "#f5f5f5", + }, + tooltip: { + backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", + color: themeMode === "dark" ? "#ffffff" : "#000000", + border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", + }, + chipStyle: { + backgroundColor: themeMode === "dark" ? "#333333" : "#F5F5F5", + borderColor: themeMode === "dark" ? "#444444" : "#E0E0E0", + color: themeMode === "dark" ? "#FFFFFF" : "#333333", + }, + defaultImage: "/images/no_image.png", + singulOrange: "/images/singul_orange.png", + singulGreen: "/images/singul_green.png", + singulBlackWhite: "/images/singul_black_white.png", + scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", + scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", + }, + typography: { + fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, + color: themeMode === "dark" ? "#ffffff" : "#000000", + useNextVariants: true, + fontWeightLight: 300, + fontWeightRegular: 400, + fontWeightMedium: 500, + fontWeightSemiBold: 600, + fontWeightBold: 700, + allVariants: { + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", + }, + h1: { + fontSize: 40, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + h2: { + fontSize: 36, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + h3: { + fontSize: 32, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + h4: { + fontSize: 30, + fontWeight: 500, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + h6: { + fontSize: 22, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + body1: { + fontSize: 16, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + body2: { + fontSize: 14, + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', + borderRadius: '4px', + }, + }, + variants: [ + { + props: { variant: 'text', color: 'primary' }, + style: { + color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", + whiteSpace: "nowrap", + textWrap: "normal", + }, + }, + { + props: { variant: 'text', color: 'secondary' }, + style: { + color: themeMode === "dark" ? "#9E9E9E" : "#616161", + whiteSpace: "nowrap", + textWrap: "normal", + }, + }, + { + props: { variant: 'contained', color: 'primary' }, + style: { + backgroundColor: themeMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35', + color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', + borderRadius: '4px', + whiteSpace: "nowrap", + textWrap: "normal", + '&:hover': { + fontWeight: 600, + backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', + color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', + }, + }, + }, + { + props: { variant: 'contained', color: 'secondary' }, + style: { + backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', + color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', + borderRadius: '4px', + boxShadow: 'none', + whiteSpace: "nowrap", + textWrap: "normal", + '&:hover': { + fontWeight: 600, + border: themeMode === "dark" ? '1px solid #f1f1f1' : 'none', + backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', + color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', + }, + }, + }, + { + props: { variant: 'outlined', color: 'primary' }, + style: { + borderColor: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", + color: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", + whiteSpace: "nowrap", + fontWeight: 'normal', + textWrap: "normal", + '&:hover': { + backgroundColor: themeMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc", + color: themeMode === "dark" ? "#1a1a1a" : "#8a3d00", + fontWeight: 600, + }, + }, + }, + { + props: { variant: 'outlined', color: 'secondary' }, + style: { + border: '1px solid #C5C5C5', + color: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', + whiteSpace: "nowrap", + textWrap: "normal", + '&:hover': { + backgroundColor: themeMode === "dark" ? '#C5C5C5' : '#EFEFEF', + borderColor: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', + fontWeight: 600, + color: themeMode === "dark" ? '#1a1a1a' : '#1A1A1A', + }, + }, + }, + ], + }, + MuiTab: { + styleOverrides: { + root: { + color: themeMode === "dark" ? "#C5C5C5" : "#1A1A1A", + }, + }, + }, + }, + + overrides: { + MuiMenu: { + list: { + backgroundColor: themeMode === "dark" ? "#27292d" : "#ffffff", + }, + }, + MuiCssBaseline: { + MuiCssBaseline: { + styleOverrides: ` + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'); + } + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'); + } + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'); + } + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + font-weight: 600; + src: local('Roboto SemiBold'), local('Roboto-SemiBold'); + } + @font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: local('Roboto Bold'), local('Roboto-Bold'); + } + `, + }, + }, + }, + }); diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx index 45d933b9..f94c3441 100644 --- a/frontend/src/views/Admin2.jsx +++ b/frontend/src/views/Admin2.jsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import AdminNavBar from '../components/AdminNavBar.jsx'; import { toast } from "react-toastify"; +import { Context } from '../context/ContextApi.jsx'; const Admin2 = (props) => { // Destructure props if needed @@ -10,13 +11,15 @@ const Admin2 = (props) => { const [selectedOrganization, setSelectedOrganization] = useState({}); const [organizationFeatures, setOrganizationFeatures] = useState({}); const [orgRequest, setOrgRequest] = React.useState(true); + const [isOrgLoaded, setIsOrgLoaded] = React.useState(false); + const {brandName} = useContext(Context) const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; if (document !== undefined) { - if (selectedOrganization?.name !== undefined) { - document.title = selectedOrganization?.name + " - Admin - Shuffle" + if (selectedOrganization?.name !== undefined) { + document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`; } else { - document.title = "Admin - Shuffle" + document.title = brandName?.length > 0 ? `Admin - ${brandName}` : `Admin - Shuffle`; } } @@ -76,6 +79,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"); } @@ -139,6 +154,8 @@ const Admin2 = (props) => { .catch((error) => { console.log("Error getting org: ", error); toast("Error getting current organization"); + }).finally(() => { + setIsOrgLoaded(true) }); }; @@ -220,7 +237,8 @@ const Admin2 = (props) => { defaults, sso_config, lead_info, - { mfa_required } = {} + { mfa_required } = {}, + editing, ) => { const data = { name: name, @@ -231,6 +249,7 @@ const Admin2 = (props) => { sso_config: sso_config, lead_info: lead_info, mfa_required: mfa_required !== undefined ? mfa_required : selectedOrganization?.mfa_required, + editing: editing?.length > 0 ? editing : "", }; const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; @@ -268,8 +287,8 @@ const Admin2 = (props) => { const handleStatusChange = (event) => { - const { value } = event.target; - setSelectedStatus(value); + const { value } = event.target + setSelectedStatus(value) handleEditOrg( selectedOrganization?.name, @@ -317,8 +336,9 @@ const Admin2 = (props) => { } return ( -
- + //
+
+
); }; diff --git a/frontend/src/views/AdminSetup.jsx b/frontend/src/views/AdminSetup.jsx index f8f3d747..65467300 100755 --- a/frontend/src/views/AdminSetup.jsx +++ b/frontend/src/views/AdminSetup.jsx @@ -1,6 +1,8 @@ /* eslint-disable react/no-multi-comp */ import React, { useState } from "react"; import { makeStyles } from "@mui/styles"; +import theme from '../theme.jsx'; +import { useNavigate } from "react-router-dom"; import { CircularProgress, @@ -20,11 +22,8 @@ const surfaceColor = "#27292D"; const inputColor = "#383B40"; const boxStyle = { - paddingLeft: "30px", - paddingRight: "30px", - paddingBottom: "30px", - paddingTop: "30px", - backgroundColor: surfaceColor, + padding: 40, + backgroundColor: theme.palette.backgroundColor, }; const useStyles = makeStyles({ @@ -41,8 +40,10 @@ const AdminAccount = (props) => { const [firstRequest, setFirstRequest] = useState(true); const [loginLoading, setLoginLoading] = useState(false); + // Used to swap from login to register. True = login, false = register const register = true; + let navigate = useNavigate(); const classes = useStyles(); // Error messages etc @@ -67,16 +68,25 @@ const AdminAccount = (props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); + setLoginInfo(responseJson["reason"]) + + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + navigate("/loginsetup") + } + } else { if (responseJson.reason === "redirect") { - window.location.pathname = "/login"; + setTimeout(() => { + window.location.pathname = "/login" + }, 2500) } + } }) ) .catch((error) => { - setLoginInfo("Error in userdata: ", error); + setLoginInfo("Error in userdata (1): ", error); + navigate("/loginsetup") }); }; @@ -107,14 +117,18 @@ const AdminAccount = (props) => { if (responseJson["success"] === false) { setLoginInfo(responseJson["reason"]); } else { - setLoginInfo("Successful register :)"); - window.location.pathname = "/login"; + setLoginInfo("Successful register! Redirecting in a moment..."); + + setTimeout(() => { + window.location.pathname = "/login"; + }, 2500) } }) ) .catch((error) => { - setLoginLoading(false); - setLoginInfo("Error in userdata: ", error); + setLoginInfo("Error in userdata (2): ", error); + setLoginLoading(false) + navigate("/loginsetup") }); }; diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx new file mode 100644 index 00000000..ebc5a558 --- /dev/null +++ b/frontend/src/views/AgentUI.jsx @@ -0,0 +1,542 @@ +import React, { useState, useEffect, useContext, memo } from "react"; +import { Context } from "../context/ContextApi.jsx"; +import { getTheme } from "../theme.jsx"; +import { toast } from "react-toastify" +import ReactJson from "react-json-view-ssr"; +import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx"; + +import { + Button, + ButtonGroup, + Typography, + Chip, + CircularProgress, + Tooltip, + IconButton, +} from '@mui/material' + +import { + CheckCircle as CheckCircleIcon, + HourglassDisabled as HourglassDisabledIcon, + RestartAlt as RestartAltIcon, + ExpandMore as ExpandMoreIcon, + ExpandLess as ExpandLessIcon, +} from '@mui/icons-material' + +import { + green, + red, +} from '../views/AngularWorkflow.jsx' + +const AgentUI = (props) => { + const { globalUrl, isLoggedIn, isLoaded, userdata, } = props + const [buttonState, setButtonState] = useState("timeline") + const [execution, setExecution] = useState(null) + const [agentActionResult, setAgentActionResult] = useState(null) + const [data, setData] = useState({}) + const [openIndexes, setOpenIndexes] = useState([]) + const [disableButtons, setDisableButtons] = useState(false) + + const [originalStartTime, setOriginalStartTime] = useState(0) + const [latestEndTime, setLatestEndTime] = useState(0) + + const {themeMode} = useContext(Context) + const theme = getTheme(themeMode) + + const agentWrapperStyle = { + width: 1000, + height: 1000, + margin: "auto", + paddingTop: 100, + } + + if (data.input === undefined || data.input === null) { + data.input = "" + } else { + const verifiedInput = validateJson(data.input) + if (verifiedInput.valid === true) { + data.input = JSON.stringify(verifiedInput.result, null, 2) + } + } + + const findNodeData = (execution_data, node_id) => { + if (execution_data === undefined || execution_data === null) { + return + } + + var found = false + for (var key in execution_data.results) { + const item = execution_data.results[key] + if (item?.action?.id !== node_id) { + continue + } + + setAgentActionResult(item) + + found = true + const validate = validateJson(item.result) + if (validate.valid) { + setData(validate.result) + } else { + toast.warn("Action output result is not valid JSON!") + } + + break + } + + if (found === false) { + toast.warn("Failed to find the relevant AI Agent result") + } + } + + const GetExecution = (execution_id, node_id, authorization) => { + if (execution_id === undefined || execution_id === null) { + toast.error("No execution ID provided. Please provide execution_id in the URL.") + return + } + + if (node_id === undefined || node_id === null) { + toast.error("No node ID provided. Please provide node_id in the URL.") + return + } + + if (authorization === undefined || authorization === null) { + toast.error("No authorization provided. Please provide authorization in the URL.") + return + } + + const headers = {} + const executionRequest = { + "execution_id": execution_id, + "authorization": authorization, + } + + const url = `${globalUrl}/api/v1/streams/results` + fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(executionRequest), + credentials: "include", + cors: "no-cors", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + if (responseJson.success !== false) { + if (responseJson.status === "EXECUTING") { + // Recursively looking for updates until it's not executing anymore + setTimeout(() => { + GetExecution(execution_id, node_id, authorization) + }, 3000) + } else { + setDisableButtons(false) + setDisableButtons(false) + } + + setExecution(responseJson) + findNodeData(responseJson, node_id) + } else { + setDisableButtons(false) + if (responseJson.reason === undefined || responseJson.reason === null) { + toast.error("Failed to load the agent data. Please try again and contact support@shuffler.io if this persists") + } else { + toast.error("Error: " + responseJson.reason) + } + } + }) + .catch((error) => { + setDisableButtons(false) + toast.error("Error: " + error) + }) + } + + const RerunDecision = (decision) => { + if (execution.execution_id === undefined || execution.execution_id === null) { + toast.error("No workflow run loaded. Please try again and contact support@shuffler.io if this persists.") + return + } + + if (agentActionResult === undefined || agentActionResult === null) { + toast.error("Failed to find the relevant agent action. Please try again, and contact support@shuffler.io if it persists.") + return + } + + console.log("DECISION: ", decision) + + const url = `${globalUrl}/api/v1/apps/agent/run?rerun=true&decision_id=${decision?.run_details?.id}` + var body = agentActionResult.action + body.source_execution = execution.execution_id + body.source_workflow = execution.workflow.id + + fetch(url, { + method: "POST", + body: JSON.stringify(body), + credentials: "include", + cors: "no-cors", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + console.log("RESP: ", responseJson) + if (responseJson.success !== false) { + } else { + if (responseJson.reason === undefined || responseJson.reason === null) { + toast.warn("Failed to restart the agent decision. Please try again and contact support@shuffler.io if this persists") + } else { + toast.warn(responseJson.reason) + + } + } + + GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization) + }) + .catch((error) => { + toast.error("Error: " + error) + }) + } + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const executionId = params.get("execution_id") + const nodeId = params.get("node_id") + const authorization = params.get("authorization") + if (executionId !== undefined && executionId !== null && nodeId !== undefined && nodeId !== null && authorization !== undefined && authorization !== null) { + GetExecution(executionId, nodeId, authorization) + } else { + toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.") + } + }, []) + + const maxTimelineWidth = 150 + const TimelineItem = (props) => { + const { item, index } = props; + const [hovered, setHovered] = useState(false); + + const parsedStatus = item.status === "RUNNING" || item.status === "WAITING" ? + + : + item.status === "FINISHED" ? + + + + : + + + + + const categoryStyle = { + width: 20, + height: 20, + marginRight: 10, + } + + const parsedCategory = item.category === "singul" ? + + + + : item.category === "ask" ? + + + + : +
+ + const validate = validateJson(item.details) + const itemStartTime = item.start_time + var itemEndTime = item.end_time + if (itemStartTime !== undefined && (itemStartTime < originalStartTime || originalStartTime === 0)) { + setOriginalStartTime(itemStartTime) + } + + if (itemEndTime !== undefined && itemEndTime > latestEndTime) { + setLatestEndTime(itemEndTime) + } + + if (itemEndTime === undefined || itemEndTime === null) { + // Set it to now + itemEndTime = latestEndTime + } + + const totalDuration = latestEndTime - originalStartTime + const currentDuration = itemStartTime - itemEndTime + var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth + var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth + + if (totalDuration === currentDuration) { + timelineMarginLeft = 0 + timelineWidth = maxTimelineWidth + } + + const defaultTopPadding = 10 + const open = openIndexes.includes(index) + + return ( +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={(e) => { + if (item.details === undefined || item.details === null || item.details === "") { + toast("No details to open") + return + } + + if (openIndexes.includes(index)) { + setOpenIndexes(openIndexes.filter((i) => i !== index)) + } else { + setOpenIndexes([...openIndexes, index]) + } + }} + > +
+ {parsedStatus} +
+
+ {parsedCategory} +
+
+ {/* To ISO string from unix time */} + {new Date(item.start_time * 1000).toLocaleString()} +
+
+ +
+
+ {item.label} +
+ + +
+ {currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ? +
+ : null} +
+ + +
+ + + { + e.preventDefault() + e.stopPropagation() + + toast.info("Attempting to rerun this decision by itself.") + setDisableButtons(true) + RerunDecision(item.details) + }} + > + + + + + + + + {open ? + + : + + } + + + + +
+
+ + {open ? +
+ {validate.valid === true ? + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + enableClipboard={(copy) => { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(validate.result, select, "exec") + }} + name={false} + /> + : + + {item.details} + + } +
+ : null } +
+ ) + } + + const TimelineRender = (props) => { + const { agent_data } = props; + var timelineItems = [ + { + "label": "AI Agent 2", + "type": "agent", + "category": "agent", + + "status": agent_data.status, + "start_time": agent_data.started_at, + "end_time": agent_data.completed_at, + }, + ] + + var sortedTimelineItems = [] + for (var key in agent_data?.decisions) { + const item = agent_data.decisions[key] + + if (item.run_details.started_at === undefined || item.run_details.started_at === null) { + item.run_details.started_at = originalStartTime + } + + if (item.run_details.completed_at === undefined || item.run_details.completed_at === null) { + item.run_details.completed_at = Math.floor(Date.now() / 1000) + } + + var newTimelineItem = { + "label": item.action, + "type": "decision", + "category": item.category, + + "status": item.run_details.status, + "start_time": item.run_details.started_at, + "end_time": item.run_details.completed_at, + } + + newTimelineItem.details = item + timelineItems.push(newTimelineItem) + } + + timelineItems.sort((a, b) => { + if (a.start_time < b.start_time) { + return 1; + } + + return 0; + }) + + return ( +
+ {/* +
+ {agent_data?.status === "RUNNING" || agent_data?.status === "WAITING" ? + + : + agent_data?.status === "FINISHED" ? + + : + + } + + + {agent_data?.status} + +
+ */} + +
+ {timelineItems?.map((item, index) => { + return ( + + ) + })} + +
+ ) + } + + return ( +
+ {/* + + Agent Input: {data.input} + + */} + + + + + + + {buttonState === "timeline" ? + + : + null + } +
+ ) +} + +export default AgentUI diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 3904b15f..0466fb85 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2,10 +2,11 @@ import React, { useState, useEffect, useLayoutEffect, memo, useMemo, useRef, useContext } from "react"; import ReactDOM from "react-dom" -import theme from "../theme.jsx"; +import { getTheme } from "../theme.jsx"; import { useInterval } from "react-powerhooks"; import { makeStyles, } from "@mui/styles"; +import YAML from "yaml"; import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx" import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid"; import { useNavigate, Link, useParams } from "react-router-dom"; @@ -74,6 +75,7 @@ import { import { + Storage as StorageIcon, Code as CodeIcon, Folder as FolderIcon, VerifiedUser as VerifiedUserIcon, @@ -131,6 +133,7 @@ import { ArrowForward as ArrowForwardIcon, OpenInFull as OpenInFullIcon, Difference as DifferenceIcon, + DataObject as DataObjectIcon, } from "@mui/icons-material"; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; //import * as cytoscape from "cytoscape"; @@ -142,11 +145,11 @@ import edgehandles from "cytoscape-edgehandles"; import CytoscapeComponent from "react-cytoscapejs"; import Draggable from "react-draggable"; -import cytoscapestyle from "../defaultCytoscapeStyle.jsx"; +import defaultCytoscapeStyle from "../defaultCytoscapeStyle.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" -import { validateJson, collapseField, GetIconInfo } from "../views/Workflows.jsx"; +import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows.jsx"; import { GetParsedPaths, internalIds, } from "../views/Apps.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; @@ -163,6 +166,33 @@ export const triggers = [ { name: "Webhook", type: "TRIGGER", + parameters : [ + { + "name": "url", + "example": "", + "value": "" + }, + { + "name": "tmp", + "example": "", + "value": "", + }, + { + "name": "auth_headers", + "example": "", + "value": "", + }, + { + "name": "custom_response_body", + "example": "", + "value": "", + }, + { + "name": "await_response", + "example": "", + "value": "v1", + }, + ], status: "uninitialized", trigger_type: "WEBHOOK", errors: null, @@ -178,6 +208,18 @@ export const triggers = [ { name: "Schedule", type: "TRIGGER", + parameters : [ + { + "name": "cron", + "example": "", + "value": "*/25 * * * *" + }, + { + "name": "execution_argument", + "example": "", + "value": "", + }, + ], status: "uninitialized", trigger_type: "SCHEDULE", errors: null, @@ -222,6 +264,38 @@ export const triggers = [ { name: "User Input", type: "TRIGGER", + parameters: [ + { + "name": "alertinfo", + "example": "", + "value": "Do you want to continue the workflow? Start parameters: $exec", + }, + { + "name": "options", + "example": "", + "value": "boolean", + }, + { + "name": "type", + "example": "", + "value": "subflow", + }, + { + "name": "email", + "example": "", + "value": "test@test.com", + }, + { + "name": "sms", + "example": "", + "value": "0000000", + }, + { + "name": "subflow", + "example": "", + "value": "", + } + ], status: "running", large_image: "/images/workflows/UserInput2.svg", description: "Wait for user input trigger", @@ -323,6 +397,7 @@ export function SetJsonDotnotation(jsonInput, inputKey) { return jsonInput; } + //export const green = "#86c142"; export const green = "#02CB70" export const yellow = "#FECC00"; @@ -358,39 +433,18 @@ export function removeParam(key, sourceURL) { return rtn; } -const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#FF8544 !important", - }, - root: { - "& .MuiAutocomplete-listbox": { - border: "2px solid #FF8544", - color: "white", - fontSize: 18, - "& li:nth-child(even)": { - backgroundColor: "#CCC", - }, - "& li:nth-child(odd)": { - backgroundColor: "#FFF", - }, - }, - }, - inputRoot: { - color: "white", - "&:hover .MuiOutlinedInput-notchedOutline": { - borderColor: "#f86a3e", - }, - }, -}); - const splitter = "|~|"; const svgSize = 24; +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + //const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const AngularWorkflow = (defaultprops) => { const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; + const {themeMode, supportEmail, brandColor} = useContext(Context) + const theme = getTheme(themeMode, brandColor) const referenceUrl = globalUrl + "/api/v1/hooks/"; //const alert = useAlert() let navigate = useNavigate(); @@ -404,10 +458,36 @@ const AngularWorkflow = (defaultprops) => { var to_be_copied = ""; const [firstrequest, setFirstrequest] = React.useState(true); - const [cystyle] = useState(cytoscapestyle); + const cystyle = useMemo(() => defaultCytoscapeStyle(theme), [themeMode]); + // const cystyle = useMemo(() => defaultCytoscapeStyle, [themeMode]); const [cy, setCy] = React.useState(); + const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#FF8544 !important", + }, + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid #FF8544", + color: theme.palette.text.primary, + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: theme.palette.text.primary, + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, + }); + const [toolsApp, setToolsApp] = React.useState({}); const [currentView, setCurrentView] = React.useState(0); const [triggerAuthentication, setTriggerAuthentication] = React.useState({}); @@ -431,6 +511,7 @@ const AngularWorkflow = (defaultprops) => { "default", ] }); + const [appGroup, setAppGroup] = React.useState([]); const [triggerGroup, setTriggerGroup] = React.useState([]); const [executionText, setExecutionText] = React.useState(""); @@ -472,6 +553,7 @@ const AngularWorkflow = (defaultprops) => { const [authGroups, setAuthGroups] = React.useState([]) const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname; + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; // 0 = normal, 1 = just done, 2 = normal @@ -503,7 +585,7 @@ const AngularWorkflow = (defaultprops) => { const [allRevisions, setAllRevisions] = useState([]) const [menuPosition, setMenuPosition] = useState(null); const [showDropdown, setShowDropdown] = React.useState(false); - const [subflowActionList, setSubflowActionList] = React.useState([]); + const [triggerActionList, setTriggerActionList] = React.useState([]); const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); @@ -574,11 +656,18 @@ const AngularWorkflow = (defaultprops) => { }, [editWorkflowModalOpen]) useEffect(() => { - if (selectedTrigger !== undefined && selectedTrigger?.parameters !== undefined && selectedTrigger?.parameters !== null && selectedTrigger?.parameters?.length > 1) { - // Right now just setting for the subflow - setSelectedTriggerValue(selectedTrigger?.parameters[1]?.value) - } - }, [selectedTrigger]) + // Check if selectedTriggerIndex is a number >= 0 + if (isNaN(selectedTriggerIndex) || selectedTriggerIndex < 0) { + return + } + + if (selectedTrigger?.trigger_type === "SUBFLOW" && selectedTriggerIndex !== undefined && selectedTriggerIndex !== null && workflow?.triggers[selectedTriggerIndex].parameters.length > 1) { + setSelectedTriggerValue(workflow.triggers[selectedTriggerIndex].parameters[1].value || "") + } else if (selectedTrigger?.trigger_type === "USERINPUT" && selectedTriggerIndex !== undefined && selectedTriggerIndex !== null && workflow?.triggers[selectedTriggerIndex].parameters.length > 0) { + setSelectedTriggerValue(workflow.triggers[selectedTriggerIndex].parameters[0].value || "Do you want to continue the workflow? Start parameters: $exec") + } + + }, [selectedTriggerIndex, selectedTrigger, workflow]) useEffect(() => { if(selectedEdge && Object.keys(selectedEdge).length > 0){ @@ -638,10 +727,36 @@ const AngularWorkflow = (defaultprops) => { "Nothing", "Create ticket", "List tickets", + "Send Email", "Get specific ticket", ], "multiselect": true, }, + { + "name": "memory", + "value": "", + "required": true, + "description": "Whether to store the conversation in memory", + "options": [ + "Nothing", + "Shuffle Datastore", + ], + "multiselect": false, + "disabled": true, + }, + { + "name": "knowledge", + "value": "", + "required": true, + "description": "The knowledge we should inject into the context window", + "options": [ + "Nothing", + "Shuffle Files", + ], + "multiselect": false, + "disabled": true, + }, + ] }], large_image: theme.palette.singulBlackWhite, @@ -665,28 +780,22 @@ const AngularWorkflow = (defaultprops) => { "name": "action", "value": "list_tickets", "options": [ + "create_ticket", "list_tickets", "get_ticket", - "create_ticket", + "close_ticket", + "add_comment", + "update_ticket", + "search_tickets" ], "required": true, }, { "name": "fields", - "value": "", + "value": '{\n "ticket_id": "123456",\n "comment": "This is a comment"\n}', "required": false, "multiline": true, }, - /*{ - "name": "options", - "value": "deduplicate,enrich", - "required": false, - "multiselect": true, - "options": [ - "deduplicate", - "enrich", - ] - }*/ ] }, { "name": "Communication", @@ -696,8 +805,14 @@ const AngularWorkflow = (defaultprops) => { "name": "action", "value": "list_messages", "options": [ - "list_messages", "send_message", + "list_messages", + "get_message", + "search_messages", + "list_attachments", + "get_attachment", + "create_contact", + "get_contact" ], "required": true, }, @@ -714,9 +829,134 @@ const AngularWorkflow = (defaultprops) => { "label": "IAM", "parameters": [{ "name": "action", - "value": "get_kms_key", + "value": "get_asset", + "options": [ + "reset_password", + "enable_user", + "disable_user", + "get_identity", + "get_asset", + "search_identity" + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "Assets", + "description": "Available actions for Assets", + "label": "Assets", + "parameters": [{ + "name": "action", + "value": "list_assets", "options": [ - "get_kms_key", + "list_assets", + "get_asset", + "search_assets", + "search_users", + "search_endpoints", + "search_vulnerabilities" + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "Eradication", + "description": "Available actions for Eradication", + "label": "Eradication", + "parameters": [{ + "name": "action", + "value": "list_alerts", + "options": [ + "list_alerts", + "close_alert", + "get_alert", + "create_detection", + "block_hash", + "search_hosts", + "isolate_host", + "unisolate_host", + "trigger_host_scan" + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "Intel", + "description": "Available actions for Intel", + "label": "Intel", + "parameters": [{ + "name": "action", + "value": "get_ioc", + "options": [ + "get_ioc", + "create_ioc", + "search_ioc", + "update_ioc", + "delete_ioc" + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "Network", + "description": "Available actions for Network", + "label": "Network", + "parameters": [{ + "name": "action", + "value": "get_rules", + "options": [ + "get_rules", + "allow_ip", + "block_ip" + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "SIEM", + "description": "Available actions for SIEM", + "label": "SIEM", + "parameters": [{ + "name": "action", + "value": "search", + "options": [ + "search", + "list_alerts", + "close_alert", + "get_alert", + "create_detection", + "add_to_lookup_list", + "isolate_endpoint" ], "required": true, }, @@ -730,23 +970,6 @@ const AngularWorkflow = (defaultprops) => { ] }] - /* - { - "name": "Email", - "label": "Email", - "parameters": [{ - "name": "action", - "value": "list_email", - "options": [ - "list_email", - "send_mail", - ], - "required": true, - }], - }] - }] - */ - // For code editor const [codeEditorModalOpen, setCodeEditorModalOpen] = React.useState(false); const [codedata, setcodedata] = React.useState(""); @@ -762,7 +985,7 @@ const AngularWorkflow = (defaultprops) => { const [loadedApps, setLoadedApps] = React.useState([]) - const loadAppConfig = (appId, select) => { + const loadAppConfig = (appId, select, skipAppLoad) => { if (appId === undefined || appId === null || appId.length === 0) { console.log("No appId to load") return @@ -782,6 +1005,14 @@ const AngularWorkflow = (defaultprops) => { return } + // Ensures reloads don't randomly happen + const appsearchValue = document.getElementById("appsearch") + if (appsearchValue !== undefined && appsearchValue !== null) { + if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { + return + } + } + if (loadedApps.includes(appId)) { //console.log("App already loaded: ", appId) @@ -858,7 +1089,7 @@ const AngularWorkflow = (defaultprops) => { setSelectedApp(foundapp) } - if (apps === undefined || apps === null || apps.length === 0) { + if ((apps === undefined || apps === null || apps.length === 0) && skipAppLoad !== true) { console.log("No apps to update :(") getApps() return @@ -878,7 +1109,7 @@ const AngularWorkflow = (defaultprops) => { break } } else { - console.log("Found app, but no actions: ", foundapp) + //console.log("Found app, but no actions: ", foundapp) } if (cy !== undefined && cy !== null) { @@ -946,11 +1177,16 @@ const AngularWorkflow = (defaultprops) => { } useEffect(() => { - if (workflow.actions?.length == 1) { - if (workflow.actions[0].app_id == "3e320a20966d33c9b7e6790b2705f0bf") { + if (workflow?.actions?.length == 1) { + if (workflow?.actions[0].app_id == "3e320a20966d33c9b7e6790b2705f0bf") { setWorkflowAsCode(true); } } + + if (workflow?.suborg_distribution !== undefined && workflow?.suborg_distribution !== null && workflow?.suborg_distribution.length > 0) { + getChildWorkflows(workflow.id) + } + }, [workflow]); // Event for making sure app is correct @@ -1116,7 +1352,8 @@ const AngularWorkflow = (defaultprops) => { !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && - el.data("type") !== "COMMENT") { + el.data("type") !== "COMMENT" && + el.data("type") !== "RESIZE-HANDLE") { return true } @@ -1136,23 +1373,66 @@ const AngularWorkflow = (defaultprops) => { useEffect(() => { // if (subflowActionList.length === 0) { const newActionList = []; - // FIXME: Have previous execution values in here - newActionList.push({ - type: "Runtime Argument", - name: "Runtime Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: "hello", - }) - newActionList.push({ - type: "Shuffle Database", - name: "Shuffle Database", + + + if (workflowExecutions.length > 0) { + for (let execution of workflowExecutions) { + const execArg = execution.execution_argument; + if (execArg && execArg.length > 0) { + const valid = validateJson(execArg); + if (valid.valid) { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: valid.result, + }) + + break + } + } + } + } + + // Add default Runtime Argument if none were added + if (newActionList.length === 0) { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "", + }) + } + // Add Shuffle DB with cache keys if available + let cacheKey = { + type: "Shuffle DB", + name: "Shuffle DB", value: "$shuffle_cache", - highlight: "shuffle_db", + highlight: "shuffle_cache", autocomplete: "shuffle_cache", - example: "hello", - }) + example: "Hello", + } + + if (listCache?.keys?.length > 0) { + cacheKey.example = {}; + for (let item of listCache.keys) { + if (item.key) { + let itemValue = item.value ?? ""; + if (itemValue.length > 10000) { + itemValue = ""; + } + cacheKey.example[item.key.split(" ").join("_")] = { value: itemValue }; + } + } + } + + newActionList.push(cacheKey); + // FIXME: Have previous execution values in here + if ( workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && @@ -1216,7 +1496,7 @@ const AngularWorkflow = (defaultprops) => { const validated = validateJson(foundResult.result) if (validated.valid) { - exampledata = validateJson.result + exampledata = validated.result break } } @@ -1234,7 +1514,7 @@ const AngularWorkflow = (defaultprops) => { } } - setSubflowActionList(newActionList); + setTriggerActionList(newActionList); }, [workflow.workflow_variables, workflow.execution_variables, workflow, selectedTrigger]); @@ -1243,56 +1523,6 @@ const AngularWorkflow = (defaultprops) => { return } - //console.log("Failed in trigger selection: ", selectedTriggerIndex, "Trigger: ", selectedTrigger) - - var found = null - try { - for (var key in workflows) { - const curworkflow = workflows[key] - const curtrigger = curworkflow?.triggers[selectedTriggerIndex] - if (curtrigger === undefined || curtrigger === null) { - console.log("Failed in trigger selection (1): ", curworkflow) - continue - } - - if (curtrigger?.parameters === undefined || curtrigger?.parameters === null || curtrigger?.parameters.length === 0) { - console.log("Failed in trigger selection (2): ", curworkflow) - continue - } - - if (curtrigger?.parameters[0] === undefined || curtrigger?.parameters[0] === null || curtrigger?.parameters[0].value === undefined || curtrigger?.parameters[0].value === null) { - console.log("Failed in trigger selection (3): ", curworkflow) - continue - } - - if (curtrigger?.parameters[0].value === selectedTrigger?.parameters[0]?.value) { - found = curworkflow - setSubworkflow(curworkflow) - } - } - - if (found !== null) { - setSubworkflow(found) - } - } catch (e) { - console.log("Failed in trigger selection (4): ", e) - //return - } - - if (found) { - const startNode = found.actions?.find((action) => action.id === workflow?.triggers[selectedTriggerIndex]?.parameters[3]?.value) - setSubworkflowStartnode(startNode) - } - - /* - // Multi-tenant sometimes gives us shit :( - if (selectedTrigger === undefined || selectedTrigger === null || selectedTrigger.id === undefined || selectedTrigger.id === null && selectedTriggerIndex >= 0) { - if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > selectedTriggerIndex) { - setSelectedTrigger(workflow.triggers[selectedTriggerIndex]) - } - } - */ - // Check if the running state is correct or not according to allTriggers if (allTriggers !== undefined && allTriggers !== null) { // Find the active trigger @@ -1315,33 +1545,45 @@ const AngularWorkflow = (defaultprops) => { const allTriggersInOne = useTriggers.pipelines.concat(useTriggers.schedules).concat(useTriggers.webhooks) for (let triggerkey in allTriggersInOne) { const curtrigger = allTriggersInOne[triggerkey] - if (curtrigger.id === selectedTrigger.id) { - if (curtrigger.status === undefined || curtrigger.status === null) { - continue - } - - if (curtrigger.running === undefined || curtrigger.running === null) { - continue - } - - var changed = false - if (curtrigger.status !== selectedTrigger.status) { - changed = true - selectedTrigger.status = curtrigger.status - } - - if (curtrigger.running !== selectedTrigger.running) { - changed = true - selectedTrigger.running = curtrigger.running - } - - if (changed) { - //console.log("TRIGGER FIX: ", selectedTrigger) - setSelectedTrigger(selectedTrigger) - } - - break + if (curtrigger.id !== selectedTrigger.id) { + continue } + + if (curtrigger.status === undefined || curtrigger.status === null) { + continue + } + + var changed = false + if (selectedTrigger?.parameters === undefined || selectedTrigger?.parameters === null) { + changed = true + + if (selectedTrigger?.trigger_type === "SCHEDULE" && curtrigger?.frequency !== undefined && curtrigger?.frequency !== null) { + selectedTrigger.parameters = [{ + name: "cron", + value: curtrigger.frequency, + }, + { + name: "execution_argument", + value: curtrigger.argument, + }] + } + } + + if (curtrigger.status !== selectedTrigger.status) { + changed = true + selectedTrigger.status = curtrigger.status + } + + if (curtrigger.running !== selectedTrigger.running) { + changed = true + selectedTrigger.running = curtrigger.running + } + + if (changed) { + setSelectedTrigger(selectedTrigger) + } + + break } } } @@ -1500,6 +1742,8 @@ const AngularWorkflow = (defaultprops) => { if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { headers["Org-Id"] = workflow.org_id } + + setWorkflows([]) fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", @@ -1517,7 +1761,7 @@ const AngularWorkflow = (defaultprops) => { if (responseJson !== undefined) { // Sets up subflow trigger with the right info - if (trigger_index > -1) { + if (isNaN(trigger_index) === false && trigger_index > -1) { var baseSubflow = {} const trigger = workflow.triggers[trigger_index]; @@ -1547,10 +1791,10 @@ const AngularWorkflow = (defaultprops) => { if (Object.getOwnPropertyNames(baseSubflow).length > 0) { const foundAction = baseSubflow.actions.find(action => action?.id === param.value) if (foundAction !== null && foundAction !== undefined) { - setSubworkflowStartnode(foundAction); + setSubworkflowStartnode(foundAction) } } else { - setSubworkflowStartnode(param.value); + setSubworkflowStartnode(param.value) } } } @@ -1748,7 +1992,6 @@ const AngularWorkflow = (defaultprops) => { const newkeys = sortByKey(responseJson.executions, "-started_at"); setWorkflowExecutions(newkeys); - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var tmpView = new URLSearchParams(cursearch).get("execution_id"); if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) { tmpView = execution_id; @@ -1804,7 +2047,6 @@ const AngularWorkflow = (defaultprops) => { } } } else { - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var tmpView = new URLSearchParams(cursearch).get("execution_id"); if (tmpView === undefined || tmpView === null || tmpView.length === 0) { const execution_id = tmpView; @@ -1847,7 +2089,6 @@ const AngularWorkflow = (defaultprops) => { //toast("Failed loading the workflow run") console.log("Status not 200 for stream results :O!"); - //const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; //const newitem = removeParam("execution_id", cursearch); //navigate(curpath + newitem) } @@ -2091,7 +2332,6 @@ const AngularWorkflow = (defaultprops) => { } // Controls the colors and direction of execution results. - // Style is in defaultCytoscapeStyle.js const handleUpdateResults = (responseJson, executionRequest) => { if (responseJson === undefined || responseJson === null || responseJson.success === false) { stop() @@ -2240,9 +2480,11 @@ const AngularWorkflow = (defaultprops) => { console.log("Should redirect to register with redirect.") setTimeout(() => { - toast("You may not have access to this workflow.") - //window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` - window.location.href = `/workflows` + // toast("You may not have access to this workflow.") + localStorage.setItem("redirectId", props.match.params.key) + navigate("/register?view=workflows&message=You need sign up to use workflows with Shuffle"); + // window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + // window.location.href = `/workflows` }, 2500) return @@ -2280,6 +2522,7 @@ const AngularWorkflow = (defaultprops) => { var newBranches = []; var newVBranches = []; var newComments = []; + var newResizes = []; for (let cyelementsKey in cyelements) { if (cyelements[cyelementsKey].data === undefined) { continue; @@ -2464,7 +2707,40 @@ const AngularWorkflow = (defaultprops) => { //console.log(curworkflowComment) newComments.push(curworkflowComment); - } else { + } else if (type === "RESIZE-HANDLE") { + if (useworkflow.resizes === undefined || useworkflow.resizes === null) { + useworkflow.resizes = []; + } + + var curworkflowResize = useworkflow.resizes.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ); + + if (curworkflowResize === undefined) { + curworkflowResize = cyelements[cyelementsKey].data(); + } + + // Ensure width and height are properly parsed + const parsedHeight = parseInt(curworkflowResize["height"]); + if (!isNaN(parsedHeight)) { + curworkflowResize.height = parsedHeight; + } else { + curworkflowResize.height = 150; // Default value if parsing fails + } + + const parsedWidth = parseInt(curworkflowResize["width"]); + if (!isNaN(parsedWidth)) { + curworkflowResize.width = parsedWidth; + } else { + curworkflowResize.width = 200; // Default value if parsing fails + } + + // Update position from Cytoscape + curworkflowResize.position = cyelements[cyelementsKey].position(); + + newResizes.push(curworkflowResize); + } + else { toast("No handler for type: " + type); } } @@ -2495,18 +2771,25 @@ const AngularWorkflow = (defaultprops) => { } */ + // FIXME: What is this? if (cy !== undefined && cy !== null) { // scale: 0.3, // bg: "#27292d", - const cyImageData = cy.png({ - output: "base64uri", - maxWidth: 480, - maxHeight: 270, - }) + if (cy?.png !== undefined && cy?.png !== null) { + try { + const cyImageData = cy.png({ + output: "base64uri", + maxWidth: 480, + maxHeight: 270, + }) - if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { - useworkflow.image = cyImageData - } + if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { + useworkflow.image = cyImageData + } + } catch (e) { + console.log("Failed to get image data: ", e) + } + } } if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { @@ -2571,9 +2854,9 @@ const AngularWorkflow = (defaultprops) => { setSavingState(0); console.log("Workflow failed loading: ", responseJson); if (responseJson.reason !== undefined && responseJson.reason !== null) { - toast("Failed to save: " + responseJson.reason); + toast.error("Failed to save: " + responseJson.reason); } else { - toast("Failed to save. Please contact your support@shuffler.io or your local admin if this is unexpected.") + toast.error(`Failed to save. Please contact your ${supportEmail} or your local admin if this is unexpected.`) } } else { setSavingState(1); @@ -2715,10 +2998,12 @@ const AngularWorkflow = (defaultprops) => { return newExample } + /* for (var i = 0; i < found.length; i++) { //newExample = newExample.replace(found[i], "REQUIRED") newExample = newExample.replace(found[i], "REPLACE_ME") } + */ return newExample } @@ -2755,6 +3040,92 @@ const AngularWorkflow = (defaultprops) => { return true; }; + const runFromHere = (curAction) => { + if (curAction.app_id === undefined || curAction.app_id === null || curAction.app_id.length === 0) { + toast.error(`No app id found for action. Please contact ${supportEmail} if this persists`) + return + } + + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0) { + curAction.source_workflow = workflow.id + } + + // Based on the previous execution id + // Look for the "execution_id" parameter + const execFound = new URLSearchParams(cursearch).get("execution_id"); + if (execFound !== undefined && execFound !== null && execFound.length > 0) { + toast.info("Rerunning based on previously watched execution id") + curAction.source_execution = execFound + } else if (workflowExecutions !== undefined && workflowExecutions !== null && workflowExecutions.length > 0) { + curAction.source_execution = workflowExecutions[0].execution_id + } else { + toast.error("No previous execution found. Please run the workflow first.") + return + } + + setExecutionRunning(true) + setExecutionRequestStarted(true) + var headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + // Rerun makes it return execution_id + authorization + const appRunUrl = `${globalUrl}/api/v1/apps/${curAction.app_id}/run?rerun=true` + fetch(appRunUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify(curAction), + credentials: "include", + }) + .then((response) => { + setExecutionRunning(false) + setExecutionRequestStarted(false) + + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!") + } + + return response.json() + }) + .then((responseJson) => { + setExecutionRequestStarted(false) + + if (responseJson?.success === false) { + setExecutionRunning(false) + + if (responseJson?.reason !== undefined && responseJson?.reason !== null && responseJson?.reason.length > 0) { + toast.error(responseJson.reason) + } else { + toast.error(`Failed to run the action. Please try again or contact ${supportEmail}`) + } + + return + } else if (responseJson?.success === true && responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id.length > 0) { + navigate(`?execution_id=${responseJson.execution_id}&node=${curAction.id}&rerun=true`) + setExecutionRequest({ + execution_id: responseJson.execution_id, + authorization: responseJson.authorization, + }) + + setExecutionData({}) + setExecutionModalOpen(true) + setExecutionModalView(1) + start() + } + }) + .catch((error) => { + toast.error("Failed to run the action. "+error.toString()) + + setExecutionRunning(false) + setExecutionRequestStarted(false) + }) + } + const executeWorkflow = (executionArgument, startNode, hasSaved, skip_popup) => { if (hasSaved === false) { @@ -2783,11 +3154,11 @@ const AngularWorkflow = (defaultprops) => { // FIXME: Check if any node contains $exec in a param // If they do, show a popup asking if they want to execute it without an execution argument, or to use a previous one - if (skip_popup !== true && executionArgument === undefined || executionArgument === null || executionArgument.length === 0) + if (skip_popup !== true && executionArgument === undefined || executionArgument === null || executionArgument?.length === 0) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { + if (workflow?.actions !== undefined && workflow?.actions !== null && workflow?.actions?.length > 0) { var foundmissing = false - for (let actionkey in workflow.actions) { + for (let actionkey in workflow?.actions) { if (workflow.actions[actionkey].parameters === undefined || workflow.actions[actionkey].parameters === null || workflow.actions[actionkey].parameters.length === 0) { continue } @@ -3173,6 +3544,19 @@ const AngularWorkflow = (defaultprops) => { return } + for (var key in responseJson) { + const curapp = responseJson[key] + if (curapp?.actions === undefined || curapp?.actions === null || curapp?.actions?.length === 0 || curapp?.actions?.length === 1) { + loadAppConfig(curapp?.id, false, true) + } + + if (key > 10) { + console.log("Breaking on 10 sideloads of total", responseJson.length) + break + + } + } + // Find app with ID "794e51c3c1a8b24b89ccc573a3defc47" (gmail) to force-break it, // Find app with ID "3e2bdf9d5069fe3f4746c29d68785a6a" (shuffle tools) to force-break it, // as to ensure the autocorrect works. @@ -3414,6 +3798,7 @@ const AngularWorkflow = (defaultprops) => { "border-opacity": ".7", "font-size": "25px", "border-color": color, + "color": theme.palette.text.primary } const animationDuration = 150 @@ -3901,7 +4286,7 @@ const AngularWorkflow = (defaultprops) => { apps.push(responseJson.actions[index]); } - console.log("Setting used subflow apps: ", apps) + //console.log("Setting used subflow apps: ", apps) setUsedSubflowApps(apps); return apps @@ -3914,6 +4299,10 @@ const AngularWorkflow = (defaultprops) => { } const findWorkflowDiff = (parentWorkflow, childWorkflow) => { + // Ensures new memory is used + parentWorkflow = JSON.parse(JSON.stringify(parentWorkflow)) + childWorkflow = JSON.parse(JSON.stringify(childWorkflow)) + var diff = { "different": false, "environment": false, @@ -3931,9 +4320,13 @@ const AngularWorkflow = (defaultprops) => { return diff } - var parentEnvironment = "" var childEnvironment = "" + + if (parentWorkflow.triggers !== undefined && parentWorkflow.triggers !== null && parentWorkflow.triggers.length > 0) { + parentWorkflow.actions = parentWorkflow.actions.concat(parentWorkflow.triggers) + } + for (var parentKey in parentWorkflow.actions) { const parentAction = parentWorkflow.actions[parentKey] if (parentAction.environment !== undefined && parentAction.environment !== null && parentAction.environment !== "") { @@ -3945,6 +4338,19 @@ const AngularWorkflow = (defaultprops) => { } var found = false + if (childWorkflow.triggers !== undefined && childWorkflow.triggers !== null && childWorkflow.triggers.length > 0) { + for (var triggerKey in childWorkflow.triggers) { + if (childWorkflow.triggers[triggerKey].replacement_for_trigger !== parentAction.id) { + continue + } + + // Rewrapping the ID just in case + childWorkflow.triggers[triggerKey].id = childWorkflow.triggers[triggerKey].replacement_for_trigger + childWorkflow.actions.push(childWorkflow.triggers[triggerKey]) + break + } + } + for (var childKey in childWorkflow.actions) { const childAction = childWorkflow.actions[childKey] if (childAction.environment !== undefined && childAction.environment !== null && childAction.environment !== "") { @@ -3991,6 +4397,10 @@ const AngularWorkflow = (defaultprops) => { continue } + var parentworkflow = "" + var parentstartnode = "" + var childworkflow = "" + var childstartnode = "" for (var parentParamIndex in parentAction.parameters) { const parentParam = parentAction.parameters[parentParamIndex] for (var childParamIndex in childAction.parameters) { @@ -3999,20 +4409,38 @@ const AngularWorkflow = (defaultprops) => { continue } + if (childParam.name === "workflow" || childParam.name === "subflow") { + parentworkflow = parentParam.value + childworkflow = childParam.value + continue + } + + if (childParam.name == "startnode") { + parentstartnode = parentParam.value + childstartnode = childParam.value + continue + } + if (childParam.value !== parentParam.value) { actionDiff.parameters.push(childParam.name) } } } + + if (parentworkflow !== childworkflow && parentstartnode !== childstartnode) { + actionDiff.parameters.push("subflow") + } } if (actionDiff.parameters.length > 0) { actionDiff.params = true } + /* if (!found) { actionDiff.new = true } + */ if (actionDiff !== undefined && actionDiff !== null && Object.keys(actionDiff).length > 1) { actionDiff.label = parentAction.label.replaceAll("_", " ") @@ -4123,16 +4551,24 @@ const AngularWorkflow = (defaultprops) => { // Check for execution_id in URL // don't redirect if it exists - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; var execFound = new URLSearchParams(cursearch).get("execution_id"); var sessionToken = new URLSearchParams(cursearch).get("session_token"); if (execFound === null && sessionToken === null) { - toast.error(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds. If you recently deleted this workflow, speak with support@shuffler.io to recover it from a revision.`, { - autoClose: 10000, - }) + if (isCloud) { + toast.error(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds. If you recently deleted this workflow, speak with ${supportEmail} to recover it from a revision.`, { + autoClose: 10000, + }) + } else { + toast.error(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds. Contact ${supportEmail} if this is unexpected.`, { + autoClose: 10000, + }) + } setTimeout(() => { + if(!isLoggedIn){ + localStorage.setItem("redirectId", props.match.params.key); + } window.location.pathname = "/workflows"; }, 2500); @@ -4423,6 +4859,17 @@ const AngularWorkflow = (defaultprops) => { setOriginalWorkflow(responseJson) } + const tmpUi = new URLSearchParams(cursearch).get("ui"); + if ( + tmpUi !== undefined && + tmpUi !== null && + tmpUi === "yaml" + ) { + setTimeout(() => { + setupWorkflowYaml(responseJson) + }, 2500) + } + setWorkflow(responseJson); setWorkflowDone(true); @@ -4441,6 +4888,9 @@ const AngularWorkflow = (defaultprops) => { setConfigureWorkflowModalOpen(true) } + + + } }) .catch((error) => { @@ -4785,7 +5235,7 @@ const AngularWorkflow = (defaultprops) => { } } - if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { + if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule") { if (!found) { //console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) // Find how many executions it has @@ -4814,6 +5264,7 @@ const AngularWorkflow = (defaultprops) => { } } else { // Readding the icon after moving the node + /* if (!found) { const iconInfo = GetIconInfo(nodedata); const svg_pin = ``; @@ -4841,6 +5292,7 @@ const AngularWorkflow = (defaultprops) => { } else { //console.log("Node already exists - don't add descriptor node"); } + */ } } @@ -6305,11 +6757,15 @@ const AngularWorkflow = (defaultprops) => { if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0 && originalWorkflow.org_id !== undefined && originalWorkflow.org_id !== null && originalWorkflow.org_id.length > 0 && workflow.org_id === originalWorkflow.org_id) { // Allows a parent workflow to control the schedule } else if (data.replacement_for_trigger !== undefined && data.replacement_for_trigger !== null && data.replacement_for_trigger.length > 0) { - toast.warning("This schedule is controlled by the parent workflow. If you want additional schedule control, please add a custom schedule to this workflow.", { - autoClose: 30000, - }) - event.target.unselect() - return + + // No custom control on cloud + if (isCloud) { + toast.warning("This schedule is controlled by the parent workflow. If you want additional schedule control, please add a custom schedule to this workflow.", { + autoClose: 30000, + }) + event.target.unselect() + return + } } } else if (data.app_name === "Webhook" && trigger_index >= 0) { @@ -6475,6 +6931,21 @@ const AngularWorkflow = (defaultprops) => { } setSelectedComment(data); + } else if (data.type === "RESIZE-HANDLE") { + + const parentNode = cy.getElementById(data.attachedTo); + if (parentNode) { + console.log("Resizing parent node:", parentNode); + + // Get the parent node's data + const parentData = parentNode.data(); + if (parentData?.type === "COMMENT") { + + // Set the parent node's data as the selected comment + setSelectedComment(parentData); + } + } + return; // Exit after handling the resize handle } else { toast("Can't handle node type " + data.type); return; @@ -6528,7 +6999,11 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (responseJson.success === false) { - toast("Failed to auto-activate the app. Go to /apps and activate it.") + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + toast.error("Failed to auto-activate the app: " + responseJson.reason) + } else { + toast.error("Failed to auto-activate the app. Go to /apps and activate it.") + } } else { if (refresh === true) { setHighlightedApp(appid) @@ -6885,7 +7360,7 @@ const AngularWorkflow = (defaultprops) => { ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { - console.log("User Input or Shuffle Workflow") + //console.log("User Input or Shuffle Workflow") } else { toast("Can't have triggers as target of branch") event.target.remove() @@ -7116,7 +7591,6 @@ const AngularWorkflow = (defaultprops) => { data: newcybranch, }; - console.log("SHOULD STITCH WITH STARTNODE"); cy.add(edgeToBeAdded); } @@ -7440,25 +7914,9 @@ const AngularWorkflow = (defaultprops) => { } break; case 86: - if (event.ctrlKey) { - //console.log("CTRL+V") - // The below parts are handled in the function handlePaste() - /* - const clipboard = navigator.clipboard - if (clipboard === undefined || window === undefined || window === null) { - toast("Can only use cliboard over HTTPS (port 3443)") - return - } - - console.log("CLIPBOARD: ", window.clipboardData) - const pastedData = window.clipboardData.getData('Text'); - console.log("PASTED: ", pastedData) - - - //var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) - var jsonvalid = true - var parsedjson = [] - */ + console.log("CTRL+V? ctrl: ", event.ctrlKey) + if (event.ctrlKey) { + // Paste is handled in the handlePaste() function. } break; case 88: @@ -7482,30 +7940,25 @@ const AngularWorkflow = (defaultprops) => { }; const handlePaste = (event) => { - if ( - event.path !== undefined && - event.path !== null && - event.path.length > 0 - ) { + if (event.path !== undefined && event.path !== null && event.path.length > 0) { if (event.path[0].localName !== "body") { - return; + console.log("Skipping paste because body is not targeted") + return; } } - if ( - event.target !== undefined && - event.target !== null - ) { - if (event.target.localName !== "body") { - return; - } + if (event.target !== undefined && event.target !== null) { + // If it's an input area, skip paste + if (event.target.localName === "input" || event.target.localName === "textarea") { + console.log("Skipping paste because body is not targeted (1). Target: ", event?.target?.localName) + return; + } } - - event.preventDefault(); - const clipboard = (event.originalEvent || event).clipboardData.getData( - "text/plain" - ); + // Does this stop things? + //event.preventDefault() + const clipboard = (event.originalEvent || event).clipboardData.getData("text/plain") + console.log("CLIPBOARD TO PASTE: ", clipboard) try { const allnodes = cy.nodes().jsons() @@ -8414,9 +8867,9 @@ const AngularWorkflow = (defaultprops) => { } // Set the edge to be dashed - edge.style("target-arrow-color", "white") + edge.style("target-arrow-color", theme.palette.text.primary) edge.style("line-style", "solid") - edge.style("line-gradient-stop-colors", ["white", "white"]) + edge.style("line-gradient-stop-colors", [theme.palette.text.primary, theme.palette.text.primary]) } } @@ -8560,7 +9013,7 @@ const AngularWorkflow = (defaultprops) => { } if (nodedata.type !== "COMMENT") { - parsedStyle.color = "white"; + parsedStyle.color = theme.palette.text.primary; } if (event.target !== undefined && event.target !== null) { @@ -8619,9 +9072,9 @@ const AngularWorkflow = (defaultprops) => { const edgeData = event.target.data(); if (edgeData.decorator === true) { // Set color of it to white and not stripled - event.target.style("target-arrow-color", "white") + event.target.style("target-arrow-color", theme.palette.text.primary) event.target.style("line-style", "solid") - event.target.style("line-gradient-stop-colors", ["white", "white"]) + event.target.style("line-gradient-stop-colors", [theme.palette.text.primary, theme.palette.text.primary]) return; } @@ -9218,11 +9671,15 @@ const AngularWorkflow = (defaultprops) => { } insertedNodes = insertedNodes.concat(newedges); - setWorkflow(inputworkflow); + setWorkflow(inputworkflow) // Reset view for cytoscape if (cy !== undefined && cy !== null) { - cy.add(insertedNodes) + try { + cy.add(insertedNodes) + } catch (error) { + console.log("Error adding nodes to cytoscape (6): ", error) + } try { cy.fit(null, 250) @@ -9301,12 +9758,6 @@ const AngularWorkflow = (defaultprops) => { } } - //if (selectedNode.data("id") === selectedAction.id) { - // setSelectedApp({}); - // setSelectedAction({}); - //setSelectedTrigger({}); - //setSelectedTriggerIndex({}); - //} const parsedSelection = cy.$(":selected"); if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { toast("This node can't be deleted."); @@ -9501,12 +9952,196 @@ const AngularWorkflow = (defaultprops) => { }); } + const setupWorkflowYaml = (inputworkflow) => { + toast.warn("YAML exploring is an experimental feature - only visible to support users. The goal of this is to make it EASY to edit the workflow as YAML instead of just using the UI") + if (userdata?.support !== true) { + console.log("Not support: ", userdata) + return + } + + // This should be getting the workflow based on actual nodes in the workflow + // as the goal is to have it be live + var copiedWorkflow = JSON.parse(JSON.stringify(inputworkflow)) + + const removeObjects = [ + "execution_org", + "categories", + "example_argument", + "public", + "contact_info", + "published_id", + "revision_id", + "usecase_ids", + "input_questions", + "form_control", + "blogpost", + "video", + "status", + "generated", + "hidden", + "updated_by", + "validated", + "validation", + "childorg_workflow_ids", + "backup_config", + "auth_groups", + "isValid", + "workflow_as_code", + "image", + "sharing", + "owner", + "configuration", + "created", + "edited", + "last_runtime", + "due_date", + "is_valid", + "execution_environment", + "default_return_value", + "visual_branches", + "previously_saved", + "workflow_type", + "parentorg_workflow", + "suborg_distribution", + "id", + "comments", + "org_id", + + // Failing test + "spalabi", + ] + + for (var i = 0; i < removeObjects.length; i++) { + delete copiedWorkflow[removeObjects[i]] + } + + const removeActionValues = [ + "_id", + "id", + + "large_image", + "description", + "is_valid", + "isStartNode", + "sharing", + "public", + "generated", + "execution_variable", + "position", + "category", + "reference_url", + "sub_action", + "run_magic_output", + "run_magic_input", + "category_label", + "suggestions", + "parent_controlled", + "source_workflow", + "source_executions", + "app_association", + "suggestion", + "small_image", + "long_description", + "tags", + "errors", + "source_executions", + "source_execution", + "required", + "example", + "type", + + "test2", + ] + + const removeActionParamValues = [ + "id", + "multiline", + "multiselect", + "options", + "action_field", + "variant", + "configuration", + "tags", + "schema", + "skip_multicheck", + "value_replace", + "unique_toggled", + "hidden", + "error", + "example", + + "test3", + ] + + if (copiedWorkflow?.actions !== undefined && copiedWorkflow?.actions !== null && copiedWorkflow?.actions.length > 0) { + for (var key in copiedWorkflow.actions) { + for (var i = 0; i < removeActionValues.length; i++) { + delete copiedWorkflow.actions[key][removeActionValues[i]] + } + + + if (copiedWorkflow.actions[key].parameters === undefined || copiedWorkflow.actions[key].parameters === null) { + continue + } + + for (var j = 0; j < copiedWorkflow.actions[key].parameters.length; j++) { + for (var k = 0; k < removeActionParamValues.length; k++) { + delete copiedWorkflow.actions[key].parameters[j][removeActionParamValues[k]] + } + } + } + } + + if (copiedWorkflow?.triggers !== undefined && copiedWorkflow?.triggers !== null && copiedWorkflow?.triggers.length > 0) { + for (var key in copiedWorkflow.triggers) { + for (var i = 0; i < removeActionValues.length; i++) { + delete copiedWorkflow.triggers[key][removeActionValues[i]] + } + + delete copiedWorkflow.triggers[key]["app_name"] + delete copiedWorkflow.triggers[key]["app_version"] + delete copiedWorkflow.triggers[key]["name"] + delete copiedWorkflow.triggers[key]["priority"] + delete copiedWorkflow.triggers[key]["replacement_for_trigger"] + + if (copiedWorkflow.triggers[key].parameters === undefined || copiedWorkflow.triggers[key].parameters === null) { + continue + } + + for (var j = 0; j < copiedWorkflow.triggers[key].parameters.length; j++) { + for (var k = 0; k < removeActionParamValues.length; k++) { + delete copiedWorkflow.triggers[key].parameters[j][removeActionParamValues[k]] + } + } + } + } + + if (copiedWorkflow?.workflow_variables === undefined || copiedWorkflow?.workflow_variables === null || copiedWorkflow?.workflow_variables.length === 0) { + delete copiedWorkflow.workflow_variables + } + + if (copiedWorkflow?.branches === undefined || copiedWorkflow?.branches === null || copiedWorkflow?.branches.length === 0) { + delete copiedWorkflow.branches + } + + + // YAML + const sampledata = YAML.stringify(copiedWorkflow) + + navigate("?ui=yaml", { replace: true }) + + setCodeEditorModalOpen(true) + setEditorData({ + "name": "workflow yaml", + "value": sampledata, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps //useEffect(() => { if (firstrequest) { setFirstrequest(false) getWorkflow(props.match.params.key, {}) - getChildWorkflows(props.match.params.key) getRevisionHistory(props.match.params.key) loadTriggers() getApps() @@ -9517,8 +10152,6 @@ const AngularWorkflow = (defaultprops) => { setLeftSideBarOpenByClick(false) localStorage.setItem("expandLeftNav", false) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - // FIXME: Don't check specific one here const tmpExec = new URLSearchParams(cursearch).get("execution_highlight"); if ( @@ -9545,6 +10178,7 @@ const AngularWorkflow = (defaultprops) => { //navigate(`?execution_highlight=${parsed_url}`) //props.history.push(curpath + newitem); } + return; } @@ -9604,7 +10238,8 @@ const AngularWorkflow = (defaultprops) => { !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && - el.data("type") !== "COMMENT") { + el.data("type") !== "COMMENT" && + el.data("type") !== "RESIZE-HANDLE") { return true } @@ -9686,6 +10321,27 @@ const AngularWorkflow = (defaultprops) => { cy.on("mouseover", "node", (e) => onNodeHover(e)); cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + cy.on("mouseover", "node[type='RESIZE-HANDLE']", (e) => { + const nodeId = e.target.id(); + + // Check the node ID to determine the cursor style based on position + if (nodeId.includes("bottom-right")) { + cy.container().style.cursor = "nwse-resize"; // Bottom-right resize cursor + } else if (nodeId.includes("top-right")) { + cy.container().style.cursor = "nesw-resize"; // Top-right resize cursor + } else if (nodeId.includes("top-left")) { + cy.container().style.cursor = "nwse-resize"; // Top-left resize cursor + } else if (nodeId.includes("bottom-left")) { + cy.container().style.cursor = "nesw-resize"; // Bottom-left resize cursor + } else { + cy.container().style.cursor = "default"; // Default cursor for other cases + } + }); + + cy.on("mouseout", "node[type='RESIZE-HANDLE']", (e) => { + cy.container().style.cursor = ""; + }); + // Handles dragging cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); @@ -9739,9 +10395,14 @@ const AngularWorkflow = (defaultprops) => { toast("Successfully stopped schedule"); } - workflow.triggers[triggerindex].status = "stopped"; - trigger.status = "stopped"; - setSelectedTrigger(trigger); + if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) { + workflow.triggers[triggerindex].status = "stopped"; + } + + //trigger.status = "stopped"; + //console.log("TRIGGER: ", trigger) + //setSelectedTrigger(trigger); + setWorkflow(workflow); saveWorkflow(workflow) @@ -9840,7 +10501,7 @@ const AngularWorkflow = (defaultprops) => { } } - toast("Creating schedule") + toast.info("Creating schedule") var data = { name: trigger.name, frequency: workflow.triggers[triggerindex].parameters[0].value, @@ -9889,9 +10550,9 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (!responseJson.success) { - toast("Failed to set schedule: " + responseJson.reason); + toast.error("Failed to set schedule: " + responseJson.reason); } else { - toast("Successfully created schedule"); + toast.success("Successfully created schedule"); workflow.triggers[triggerindex].status = "running"; trigger.status = "running"; setSelectedTrigger(trigger); @@ -9950,7 +10611,7 @@ const AngularWorkflow = (defaultprops) => { minWidth: isMobile ? 50 : "100%", maxWidth: isMobile ? 50 : "100%", marginTop: "5px", - color: "white", + color: theme.palette.text.primary, backgroundColor: theme.palette.surfaceColor, cursor: "pointer", display: "flex", @@ -9963,7 +10624,7 @@ const AngularWorkflow = (defaultprops) => { minWidth: "100%", maxWidth: "100%", marginTop: "5px", - color: "white", + color: theme.palette.text.primary, backgroundColor: theme.palette.surfaceColor, cursor: "pointer", display: "flex", @@ -10055,7 +10716,7 @@ const AngularWorkflow = (defaultprops) => { aria-controls="long-menu" aria-haspopup="true" onClick={menuClick} - style={{ color: "white" }} + style={{ color: theme.palette.text.primary }} > @@ -10077,7 +10738,7 @@ const AngularWorkflow = (defaultprops) => { { setOpen(false); @@ -10103,7 +10764,7 @@ const AngularWorkflow = (defaultprops) => { { deleteVariable(type, index); @@ -10139,7 +10800,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" href="https://shuffler.io/docs/workflows#workflow_variables" target="_blank" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > Workflow variables? @@ -10189,7 +10850,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > Runtime variables? @@ -10504,6 +11165,7 @@ const AngularWorkflow = (defaultprops) => { large_image: data.large_image, status: "uninitialized", name: data.name, + parameters: data?.parameters, isStartNode: false, position: newposition, } @@ -10705,6 +11367,10 @@ const AngularWorkflow = (defaultprops) => { } const handleAppDrag = (e, app) => { + if (cy === undefined || cy === null) { + return + } + const cycontainer = cy.container() // Handling drag of public apps @@ -11232,11 +11898,11 @@ const AngularWorkflow = (defaultprops) => { 17 ? -3 : 8, + color: theme.palette.textPrimary }} > {newAppname} @@ -11355,7 +12021,7 @@ const AngularWorkflow = (defaultprops) => { const positionInfo = document.activeElement.getBoundingClientRect() const outerlistitemStyle = { - width: "100%", + width: "80%", overflowX: "hidden", overflowY: "hidden", borderBottom: "1px solid rgba(255,255,255,0.4)", @@ -11375,7 +12041,7 @@ const AngularWorkflow = (defaultprops) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -11399,8 +12065,9 @@ const AngularWorkflow = (defaultprops) => { var type = "app" const baseImage = + const width = 230 return ( -
+
{hits.length === 0 ? @@ -11421,7 +12088,7 @@ const AngularWorkflow = (defaultprops) => { overflowX: "hidden", overflowY: "hidden", borderBottom: "1px solid rgba(255,255,255,0.4)", - backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit", + backgroundColor: mouseHoverIndex === index ? theme.palette.hoverColor : "inherit", cursor: "pointer", marginLeft: 0, marginRight: 0, @@ -11494,7 +12161,7 @@ const AngularWorkflow = (defaultprops) => { }} defaultPosition={{ x: 0, y: 0 }} > -
{ +
{ clickedApp(hit) }}> @@ -11541,8 +12208,7 @@ const AngularWorkflow = (defaultprops) => {
{title} @@ -11631,9 +12297,12 @@ const AngularWorkflow = (defaultprops) => { /> )} - - Your Apps - + {visibleApps.length > extraApps.length ? + + Your Apps + + : null} + {visibleApps.length > extraApps.length ? (
{visibleApps.map((app, index) => { @@ -11646,7 +12315,10 @@ const AngularWorkflow = (defaultprops) => { } if ((app.id === "integration" || app.id === "shuffle_agent") && userdata.support !== true) { - return null + if (isCloud === false && app.id === "integration") { + } else { + return null + } } if (viewedApps.includes(app.id)) { @@ -11690,7 +12362,7 @@ const AngularWorkflow = (defaultprops) => { }} > - Click one of the relevant public apps below to Activate it for your organization. + Click one of the public apps below to Activate it for your organization. { console.log("CLICKED") @@ -11711,7 +12383,7 @@ const AngularWorkflow = (defaultprops) => {
) : apps.length > 0 ? (
{ console.log("Should load in extra apps?") }} @@ -11719,6 +12391,7 @@ const AngularWorkflow = (defaultprops) => { Couldn't find the apps you were looking for? Searching unactivated apps. Click one of these apps to Activate it for your organisation. + { console.log("CLICKED") }}> @@ -11796,7 +12469,7 @@ const AngularWorkflow = (defaultprops) => { const newaction = selectedApp.actions.find((a) => a.name === e.target.value) if (newaction === undefined || newaction === null) { - toast("Failed to find the action you selected. Please try again or contact support@shuffler.io if it persists."); + toast(`Failed to find the action you selected. Please try again or contact ${supportEmail} if it persists.`); return; } @@ -12053,6 +12726,8 @@ const AngularWorkflow = (defaultprops) => { const selectedTriggerChange = (event) => { selectedTrigger.label = event.target.value; setSelectedTrigger(selectedTrigger); + workflow.triggers[selectedTriggerIndex].label = event.target.value; + setWorkflow(workflow); }; // Starts on current node and climbs UP the tree to the root object. @@ -12074,7 +12749,7 @@ const AngularWorkflow = (defaultprops) => { // maxiter = max amount of parent nodes to loop // also handles breaks if there are issues var iterations = 0; - var maxiter = 10; + var maxiter = 100; while (true) { for (let parentkey in allkeys) { var currentnode = cy.getElementById(allkeys[parentkey]); @@ -12142,8 +12817,8 @@ const AngularWorkflow = (defaultprops) => { const appApiViewStyle = { display: "flex", flexDirection: "column", - backgroundColor: "#1F2023", - color: "white", + backgroundColor: theme.palette.DialogStyle.backgroundColor, + color: theme.palette.text.primary, paddingRight: 15, paddingLeft: 15, minHeight: "100%", @@ -12172,7 +12847,9 @@ const AngularWorkflow = (defaultprops) => { overflowAnchor: "none", minWidth: minSize, width: isMobile ? "100%" : minSize, - zoom: 0.9, + zoom: isSafari ? undefined : 0.9, + transform: isSafari ? "scale(0.9)" : undefined, + transformOrigin: isSafari ? "top right" : undefined, }; const setTriggerFolderWrapperMulti = (event) => { @@ -12220,16 +12897,19 @@ const AngularWorkflow = (defaultprops) => { }; const setTriggerCronWrapper = (value) => { - console.log("Cron Value: ", value) if (selectedTrigger.parameters === null) { selectedTrigger.parameters = []; } - + selectedTrigger.parameters[0] = { + value: value, + name: "cron", + }; workflow.triggers[selectedTriggerIndex].parameters[0] = { value: value, name: "cron", }; setWorkflow(workflow); + setSelectedTrigger(selectedTrigger); }; const setTriggerOptionsWrapper = (value) => { @@ -12275,11 +12955,14 @@ const AngularWorkflow = (defaultprops) => { if (selectedTrigger.parameters === null) { selectedTrigger.parameters = []; } - + selectedTrigger.parameters[0] = { + value : value + } workflow.triggers[selectedTriggerIndex].parameters[0] = { value: value, name: "alertinfo", }; + setSelectedTrigger(selectedTrigger) setWorkflow(workflow); }; @@ -12416,13 +13099,7 @@ const AngularWorkflow = (defaultprops) => { // Get parent nodes and their data if (selectedEdge) { - - let nodeId; - if (type === "source") { - nodeId = selectedEdge.source; - } else if (type === "destination") { - nodeId = selectedEdge.target; - } + const nodeId = selectedEdge.target; const node = cy.getElementById(nodeId); if (node?.length > 0) { @@ -12538,7 +13215,7 @@ const AngularWorkflow = (defaultprops) => { event.preventDefault() setExpansionModalOpen(true) setActiveDialog("codeeditor") - navigate(`?condition_id=${data.id}&field=${data.name}`) + navigate(`?condition_id=${data.id}&condition_field=${data.name}`) setEditorData({ "name": data.name, "value": data.value || "", @@ -12549,7 +13226,7 @@ const AngularWorkflow = (defaultprops) => { > @@ -12566,7 +13243,7 @@ const AngularWorkflow = (defaultprops) => { data.value !== undefined && data.value !== null && data.value.includes(".#") ? ( - + Use "Shuffle Tools" app with "Filter List" action to handle loops ) : null @@ -12623,9 +13300,9 @@ const AngularWorkflow = (defaultprops) => { // Update the field value based on type if (type === "source") { - handleConditionFieldChange("source", "value", toComplete); + handleConditionFieldChange("source", toComplete); } else if (type === "destination") { - handleConditionFieldChange("destination", "value", toComplete); + handleConditionFieldChange("destination", toComplete); } handleMenuClose(); @@ -12717,7 +13394,7 @@ const AngularWorkflow = (defaultprops) => { startIcon={} sx={{ marginLeft: 10, - color: "white", + color: theme.palette.text.primary, fontSize: "15px", fontFamily: theme?.typography?.fontFamily, textTransform: "none", @@ -12729,6 +13406,7 @@ const AngularWorkflow = (defaultprops) => { justifyContent: "flex-start", "&:hover": { backgroundColor: "transparent", + color: theme.palette.text.primary, border: "none" }, // Disable ripple effect @@ -12738,7 +13416,7 @@ const AngularWorkflow = (defaultprops) => { }} > - Auto Complete + Auto Complete { onClose={handleMenuClose} PaperProps={{ style: { - backgroundColor: "#82ccc3", - color: "white", + backgroundColor: theme.palette.backgroundColor, + color: theme.palette.text.primary, marginTop: 2, maxHeight: 400, }, @@ -12872,7 +13550,7 @@ const AngularWorkflow = (defaultprops) => { } parentMenuOpen={!!menuPosition} style={{ - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, maxHeight: 50, @@ -12891,7 +13569,7 @@ const AngularWorkflow = (defaultprops) => { style={{ // backgroundColor: theme.palette.inputColor, marginLeft: 15, - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, padding: 0, @@ -12934,7 +13612,7 @@ const AngularWorkflow = (defaultprops) => { key={pathdata.name} style={{ // backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, padding: boxPadding, @@ -12975,7 +13653,7 @@ const AngularWorkflow = (defaultprops) => { key={innerdata.name} style={{ // backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, padding: "10px 12px", // Add padding here }} value={innerdata} @@ -13008,7 +13686,7 @@ const AngularWorkflow = (defaultprops) => { }; const menuItemStyle = { - color: "white", + color: theme.palette.text.primary, backgroundColor: theme.palette.inputColor, }; @@ -13072,7 +13750,7 @@ const AngularWorkflow = (defaultprops) => { style: { padding: 30, pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, minWidth: isMobile ? "90%" : 800, border: theme.palette.defaultBorder, @@ -13095,11 +13773,11 @@ const AngularWorkflow = (defaultprops) => { setAuthgroupModalOpen(false) }} > - + - Authgroup Selection + Authgroup Selection @@ -13196,7 +13874,7 @@ const AngularWorkflow = (defaultprops) => { style: { padding: 30, pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, minWidth: isMobile ? "90%" : 650, border: theme.palette.defaultBorder, @@ -13219,11 +13897,11 @@ const AngularWorkflow = (defaultprops) => { setExecutionArgumentModalOpen(false) }} > - + - Provide an execution argument + Provide an execution argument @@ -13242,7 +13920,7 @@ const AngularWorkflow = (defaultprops) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, fontSize: "1em", }, }} @@ -13407,7 +14085,7 @@ const AngularWorkflow = (defaultprops) => { open={aiQueryModalOpen} PaperProps={{ style: { - color: "white", + color: theme.palette.text.primary, minWidth: isMobile ? "90%" : 450, border: theme.palette.defaultBorder, padding: 50, @@ -13437,7 +14115,7 @@ const AngularWorkflow = (defaultprops) => { onClick={(e) => { }} > - + { position: "absolute", top: 6, right: 6, - color: "white", + color: theme.palette.text.primary, }} onClick={() => { setAiQueryModalOpen(false) @@ -13457,7 +14135,7 @@ const AngularWorkflow = (defaultprops) => { Shuffle AI - What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Beta feature. Please give feedback to support@shuffler.io {"<"}3 + What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Beta feature. Please give feedback to {supportEmail} {"<"}3 { /> - const handleConditionFieldChange = (fieldType, fieldName, value) => { + const handleConditionFieldChange = (fieldType, value) => { if (fieldType === "source") { setSourceValue({ ...sourceValue, value: value - }); + }) + + setUpdate(Math.random()) } else if (fieldType === "destination") { setDestinationValue({ ...destinationValue, value: value - }); - } - }; + }) + + setUpdate(Math.random()) + } + } const conditionsModal = ( @@ -13527,14 +14209,23 @@ const AngularWorkflow = (defaultprops) => { aria-labelledby="draggable-dialog-title" open={conditionsModalOpen} PaperProps={{ - style: { + sx: { pointerEvents: "auto", - color: "white", - minWidth: isMobile ? "90%" : 800, + color: theme.palette.DialogStyle.color, + minWidth: isMobile ? "90%" : "800px", border: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.DialogStyle.borderRadius, + backgroundColor: theme.palette.DialogStyle.backgroundColor, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, }, }} onClose={() => { @@ -13545,7 +14236,8 @@ const AngularWorkflow = (defaultprops) => { position: "absolute", bottom: 10, left: 10, - color: "rgba(255,255,255,0.6)", + color: theme.palette.textColor, + backgroundColor: 'inherit', zIndex: 10000, }} > @@ -13556,7 +14248,7 @@ const AngularWorkflow = (defaultprops) => { href="/docs/workflows#conditions" style={{ textDecoration: "none", - color: "#FF8544", + color: theme.palette.linkColor, }} > Learn more @@ -13564,7 +14256,7 @@ const AngularWorkflow = (defaultprops) => { - Condition + Condition
@@ -13774,7 +14466,7 @@ const AngularWorkflow = (defaultprops) => {
- +
+ { ListboxProps={{ style: { backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }, }} sx={{ @@ -15106,7 +15838,7 @@ const AngularWorkflow = (defaultprops) => { }} style={{ backgroundColor: theme.palette.inputColor, - color: isParent ? "red" : "white", + color: isParent ? "red" : theme.palette.text.primary, }} value={action} > @@ -15171,14 +15903,14 @@ const AngularWorkflow = (defaultprops) => { "name": workflow.triggers[selectedTriggerIndex].parameters[1].name, "value": parsedvalue, "field_number": 1, - "actionlist": subflowActionList, + "actionlist": triggerActionList, "field_id": "subflow_exec_field", }) }} > {
@@ -15216,15 +15953,13 @@ const AngularWorkflow = (defaultprops) => { fullWidth color="primary" placeholder="Some execution data" - value={ - selectedTriggerValue - } + value={selectedTriggerValue || ""} onChange={(e) => { setLastSaved(false) setSelectedTriggerValue(e.target.value) }} - onBlur={() => { - workflow.triggers[selectedTriggerIndex].parameters[1].value = selectedTriggerValue + onBlur={(e) => { + workflow.triggers[selectedTriggerIndex].parameters[1].value = e.target.value setWorkflow(workflow) }} /> @@ -15238,20 +15973,21 @@ const AngularWorkflow = (defaultprops) => { open={!!menuPosition} style={{ border: `2px solid #FF8544`, - color: "white", + color: theme.palette.text.primary, marginTop: 2, }} > - {subflowActionList.map((innerdata) => { + {triggerActionList.map((innerdata) => { const icon = innerdata.type === "action" ? ( ) : innerdata.type === "workflow_variable" || innerdata.type === "execution_variable" ? ( - ) : ( - - ); + ) : innerdata.type === "Shuffle DB" ? + + : + const handleExecArgumentHover = (inside) => { var exec_text_field = document.getElementById( @@ -15353,7 +16089,7 @@ const AngularWorkflow = (defaultprops) => { parentMenuOpen={!!menuPosition} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, minWidth: 250, }} onClick={() => { @@ -15376,7 +16112,7 @@ const AngularWorkflow = (defaultprops) => { key={pathdata.name} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, minWidth: 250, }} value={pathdata} @@ -15409,7 +16145,7 @@ const AngularWorkflow = (defaultprops) => { } parentMenuOpen={!!menuPosition} style={{ - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, maxHeight: 50, @@ -15427,7 +16163,7 @@ const AngularWorkflow = (defaultprops) => { style={{ // backgroundColor: theme.palette.inputColor, marginLeft: 15, - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, padding: 0, @@ -15470,7 +16206,7 @@ const AngularWorkflow = (defaultprops) => { key={pathdata.name} style={{ // backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, minWidth: 250, maxWidth: 250, padding: boxPadding, @@ -15511,7 +16247,7 @@ const AngularWorkflow = (defaultprops) => { key={innerdata.name} style={{ // backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }} value={innerdata} onMouseOver={() => handleMouseover()} @@ -15605,7 +16341,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://shuffler.io/docs/workflows#comments" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > What are comments? @@ -15621,11 +16357,14 @@ const AngularWorkflow = (defaultprops) => {
Name
{ setSelectedComment(selectedComment); }} /> +
+
+
Justify
+ +
+
+
Align
+ +
+
Height
@@ -15743,7 +16555,6 @@ const AngularWorkflow = (defaultprops) => { defaultValue={selectedComment["backgroundimage"]} onChange={(event) => { selectedComment.backgroundimage = event.target.value; - console.log("Comment: ", selectedComment) setSelectedComment(selectedComment); }} /> @@ -15757,20 +16568,7 @@ const AngularWorkflow = (defaultprops) => { // Special SCHEDULE handler var trigger_header_auth = "" if (Object.getOwnPropertyNames(selectedTrigger)?.length > 0 && workflow?.triggers !== null && workflow?.triggers !== undefined && workflow?.triggers?.length >= selectedTriggerIndex && workflow?.triggers[selectedTriggerIndex] !== undefined) { - if (selectedTrigger.trigger_type === "SCHEDULE" && workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null) { - console.log("Autofixing schedule") - - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "cron", - value: isCloud ? "*/25 * * * *" : "60", - }; - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "execution_argument", - value: '{"name": "value"}', - }; - setWorkflow(workflow); - } else if (selectedTrigger.trigger_type === "WEBHOOK") { + if (selectedTrigger.trigger_type === "WEBHOOK") { if (workflow.triggers[selectedTriggerIndex] === undefined) { return null; } @@ -15819,52 +16617,11 @@ const AngularWorkflow = (defaultprops) => { workflow.triggers[selectedTriggerIndex].parameters.length > 2 ? workflow.triggers[selectedTriggerIndex].parameters[2].value : ""; - } else if ( - selectedTrigger.trigger_type === "USERINPUT" - ) { - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow?.triggers[selectedTriggerIndex]?.parameters?.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "alertinfo", - value: "Do you want to continue the workflow? Start parameters: $exec", - }; - - // boolean, - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "options", - value: "boolean", - }; - - // email,sms,app ... - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "type", - value: "subflow", - }; - - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "email", - value: "test@test.com", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "sms", - value: "0000000", - }; - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "subflow", - value: "", - }; - - setWorkflow(workflow); - } } } const WebhookSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || workflow?.triggers[selectedTriggerIndex] === undefined || selectedTrigger?.trigger_type !== "WEBHOOK" ? null : -
+

{selectedTrigger.app_name}: {selectedTrigger.status}

@@ -15872,7 +16629,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://shuffler.io/docs/triggers#webhook" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > What are webhooks? @@ -15894,6 +16651,7 @@ const AngularWorkflow = (defaultprops) => { fullWidth color="primary" placeholder={selectedTrigger.label} + defaultValue={selectedTrigger?.label} onChange={selectedTriggerChange} disabled={selectedTrigger?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0} /> @@ -15907,7 +16665,7 @@ const AngularWorkflow = (defaultprops) => { ListboxProps={{ style: { backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }, }} sx={{ @@ -16069,7 +16827,7 @@ const AngularWorkflow = (defaultprops) => { }} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, height: 50, }} > @@ -16081,7 +16839,7 @@ const AngularWorkflow = (defaultprops) => { return ( {data} @@ -16141,7 +16899,7 @@ const AngularWorkflow = (defaultprops) => { ].parameters[0].value.includes("127.0.0.1")) ? ( PS: This does NOT work with localhost. Use your local IP instead. @@ -16256,13 +17014,15 @@ const AngularWorkflow = (defaultprops) => {
{ }} InputProps={{ style: { + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} fullWidth @@ -16309,13 +17069,15 @@ const AngularWorkflow = (defaultprops) => {
{ }} InputProps={{ style: { + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} fullWidth @@ -16371,7 +17133,7 @@ const AngularWorkflow = (defaultprops) => { value="await_response" /> } - label={
Wait For Response
} + label={
Wait For Response
} /> : null} @@ -16632,7 +17394,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://shuffler.io/docs/triggers#user_input" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > What is the user input trigger? @@ -16648,16 +17410,19 @@ const AngularWorkflow = (defaultprops) => {
Name
0} /> @@ -16671,7 +17436,7 @@ const AngularWorkflow = (defaultprops) => { }} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, marginLeft: "5px", maxWidth: "95%", height: 50, @@ -16687,41 +17452,394 @@ const AngularWorkflow = (defaultprops) => {
*/}
-
-
- Information - - The information you want to show the user. Supports variables. Supports Markdown & HTML. - -
+
+
+ Information + + { + event.preventDefault() + setCodeEditorModalOpen(true) + setActiveDialog("codeeditor") + var parsedvalue = workflow?.triggers[selectedTriggerIndex]?.parameters[0]?.value + + navigate(`?trigger_id=${selectedTrigger.id}&trigger_field=${"alertinfo"}&trigger_name=${selectedTrigger.label}`) + setEditorData({ + "name": workflow.triggers[selectedTriggerIndex].parameters[0].name, + "value": parsedvalue, + "field_number": 0, + "actionlist": triggerActionList, + "field_id": "information_field", + }) + }} + > + + + +
+ + The information you want to show the user. Supports variables. Supports Markdown & HTML. +
+ + { + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + //setShowDropdownNumber(3) + setShowDropdown(true); + }} + /> + + + ), }} fullWidth rows="4" multiline - defaultValue={ - workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers[selectedTriggerIndex].parameters !== undefined && workflow.triggers[selectedTriggerIndex].parameters.length > 0 && workflow.triggers[selectedTriggerIndex].parameters[0] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[0].value : "" - } + value={selectedTriggerValue || ""} color="primary" placeholder="" + onChange={(e) => { + setLastSaved(false) + setSelectedTriggerValue(e.target.value) + }} onBlur={(e) => { + setLastSaved(false) setTriggerTextInformationWrapper(e.target.value); }} /> + {!showDropdown ? null : + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + border: `2px solid #FF8544`, + color: theme.palette.text.primary, + marginTop: 2, + }} + > + {triggerActionList.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : innerdata.type === "Shuffle DB" ? + + : + + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #FF8544"; + } else { + exec_text_field.style.border = ""; + } + } + + // Also doing arguments + if ( + workflow.triggers !== undefined && + workflow.triggers !== null && + workflow.triggers.length > 0 + ) { + for (let triggerkey in workflow.triggers) { + const item = workflow.triggers[triggerkey]; + + if (cy !== undefined && cy !== null) { + var node = cy.getElementById(item.id); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + } + } + } + + const handleActionHover = (inside, actionId) => { + if (cy !== undefined && cy !== null) { + var node = cy.getElementById(actionId); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + }; + + const handleMouseover = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + + return parsedPaths.length > 0 ? ( + + {/* + + {icon} {innerdata.name} +
+ } + parentMenuOpen={!!menuPosition} + style={{ + backgroundColor: theme.palette.inputColor, + color: theme.palette.text.primary, + minWidth: 250, + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ) + + return ( + { }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
+ {icon} {pathdata.name} +
+
+
+ ); + })} + + */} + + + {icon} {innerdata.name} +
+ } + parentMenuOpen={!!menuPosition} + style={{ + color: theme.palette.text.primary, + minWidth: 250, + maxWidth: 250, + maxHeight: 50, + overflow: "hidden", + }} + onClick={() => { + console.log(innerdata.example) + handleItemClick([innerdata]); + }} + > + + + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + + {innerdata.name} + + + + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + // + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ); + // + + const indentation_count = (pathdata.name.match(/\./g) || []).length + 1 + const baseIndent =
+ //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 + const boxPadding = 0 + const namesplit = pathdata.name.split(".") + const newname = namesplit[namesplit.length - 1] + return ( + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
+ {Array(indentation_count).fill().map((subdata, subindex) => { + return ( + baseIndent + ) + })} + {icon} {newname} + {pathdata.type === "list" ? { + + }} /> : null} +
+
+
+ ); + })} + + + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + +
+ {icon} {innerdata.name} +
+
+
+ ); + })} + + }
{ value="subflow" /> } - label={
Subflow
} + label={
Subflow
} /> { value="email" /> } - label={
Email
} + label={
Email
} /> { disabled={true} /> } - label={
SMS
} + label={
SMS
} /> {workflow?.triggers && @@ -16804,7 +17922,7 @@ const AngularWorkflow = (defaultprops) => { ListboxProps={{ style: { backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }, }} sx={{ @@ -16860,7 +17978,7 @@ const AngularWorkflow = (defaultprops) => { }> { @@ -16881,12 +17999,32 @@ const AngularWorkflow = (defaultprops) => { }} renderInput={(params) => { return ( - +
+ + + {subworkflow === null || subworkflow === undefined || subworkflow?.id === undefined || subworkflow?.id === null || subworkflow?.id.length === 0 ? null : + + + + + + } +
); }} /> @@ -16925,13 +18063,14 @@ const AngularWorkflow = (defaultprops) => { ? ( { workflow?.triggers[selectedTriggerIndex].parameters[2] && workflow?.triggers[selectedTriggerIndex].parameters[2].value && ( - workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("email") || workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") ) ? ( {
- const defaultEnvironment = environments.find( - (env) => env.default && env.Name.toLowerCase() !== "cloud" + const defaultEnvironment = environments && environments?.find( + (env) => env?.default && env?.Name?.toLowerCase() !== "cloud" ); if (selectedTrigger.trigger_type === "PIPELINE" && selectedTrigger.environment === "onprem" && defaultEnvironment !== undefined) { @@ -17067,7 +18205,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://shuffler.io/docs/triggers#pipelines" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor}} > What are pipelines? @@ -17114,7 +18252,7 @@ const AngularWorkflow = (defaultprops) => { }} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, height: 50, }} > @@ -17132,7 +18270,7 @@ const AngularWorkflow = (defaultprops) => { key={data.id} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }} value={data.Name} > @@ -17329,7 +18467,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://shuffler.io/docs/triggers#schedule" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > What are schedules? @@ -17345,16 +18483,19 @@ const AngularWorkflow = (defaultprops) => {
Name
0} /> @@ -17373,9 +18514,9 @@ const AngularWorkflow = (defaultprops) => { fullWidth onChange={(e) => { selectedTrigger.environment = e.target.value; - setSelectedTrigger(selectedTrigger); if (e.target.value === "cloud") { console.log("Set cloud config"); + selectedTrigger.parameters[0].value = "*/25 * * * *"; workflow.triggers[selectedTriggerIndex].parameters[0].value = "*/25 * * * *"; } else { @@ -17384,13 +18525,13 @@ const AngularWorkflow = (defaultprops) => { workflow.triggers[selectedTriggerIndex].parameters[0].value = "60"; } - + setSelectedTrigger(selectedTrigger); setWorkflow(workflow); setUpdate(Math.random()); }} style={{ - backgroundColor: theme.palette.inputColor, - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, height: 50, }} > @@ -17402,7 +18543,7 @@ const AngularWorkflow = (defaultprops) => { return ( {data} @@ -17439,16 +18580,18 @@ const AngularWorkflow = (defaultprops) => { }} />
- When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"} + When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"}
{
{ const topBarStyle = { position: "absolute", - top: isMobile ? 30 : 25, + top: isMobile ? 30 : isSafari ? 15 : 25, transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, transition: "all 0.3s ease", - zoom: 0.9, + zoom: isSafari ? undefined : 0.9, } + + const changeOrg = () => { + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + fetch(`${globalUrl}/api/v1/orgs/${workflow.org_id}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify({ "org_id": workflow.org_id }), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + } + + return response.json(); + }) + .then(function (responseJson) { + console.log("In here?") + if (responseJson.success === true) { + if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { + console.log("Region Change: ", responseJson.region_url); + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + if (responseJson["reason"] === "SSO_REDIRECT") { + setTimeout(() => { + toast.info("Redirecting to SSO login page as SSO is required for this organization.") + window.location.href = responseJson["url"] + return + }, 2000) + } else { + setTimeout(() => { + window.location.reload(); + }, 2000); + } + + toast.info("Successfully changed active organisation - refreshing!"); + } else { + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + toast(responseJson.reason); + } else { + toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`); + } + } + }) + .catch((error) => { + console.log("error changing: ", error); + //removeCookie("session_token", {path: "/"}) + }) + } + const TopCytoscapeBar = (props) => { const [hovered, setHovered] = useState(false) @@ -17652,72 +18859,15 @@ const AngularWorkflow = (defaultprops) => { )} - {!distributedFromParent ? - isCorrectOrg ? null : + {!distributedFromParent || userdata?.support === true ? + isCorrectOrg ? null : Warning: { toast("Changing to correct organisation. Please wait a few seconds.") - localStorage.setItem("globalUrl", ""); - localStorage.setItem("getting_started_sidebar", "open"); - fetch(`${globalUrl}/api/v1/orgs/${workflow.org_id}/change`, { - mode: "cors", - credentials: "include", - crossDomain: true, - method: "POST", - body: JSON.stringify({ "org_id": workflow.org_id }), - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } else { - localStorage.removeItem("apps") - localStorage.removeItem("workflows") - localStorage.removeItem("userinfo") - } - - return response.json(); - }) - .then(function (responseJson) { - console.log("In here?") - if (responseJson.success === true) { - if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { - console.log("Region Change: ", responseJson.region_url); - localStorage.setItem("globalUrl", responseJson.region_url); - //globalUrl = responseJson.region_url - } - - if (responseJson["reason"] === "SSO_REDIRECT") { - setTimeout(() => { - toast.info("Redirecting to SSO login page as SSO is required for this organization.") - window.location.href = responseJson["url"] - return - }, 2000) - } else { - setTimeout(() => { - window.location.reload(); - }, 2000); - } - - toast("Successfully changed active organisation - refreshing!"); - } else { - if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { - toast(responseJson.reason); - } else { - toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); - } - } - }) - .catch((error) => { - console.log("error changing: ", error); - //removeCookie("session_token", {path: "/"}) - }) + changeOrg() @@ -17736,7 +18886,14 @@ const AngularWorkflow = (defaultprops) => { {originalWorkflow?.suborg_distribution === undefined || originalWorkflow?.suborg_distribution === null || originalWorkflow?.suborg_distribution?.length === 0 || originalWorkflow?.suborg_distribution.includes("none") ? - originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0 ? +
+ {originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0 ? + + null + + /* + * Disabled because redirects occurred anyway + - : userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 ? + */ + + : null} + + {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 && userdata?.active_org?.creator_org?.length === 0 && userdata?.active_org?.id == workflow?.org_id ? - : null - + : null} +
: + Save the workflow first : null} arrow placement="right"> @@ -17784,7 +18947,7 @@ const AngularWorkflow = (defaultprops) => { Select an Org ({originalWorkflow?.suborg_distribution?.length}) @@ -17792,7 +18955,7 @@ const AngularWorkflow = (defaultprops) => { style={{ pointerEvents: "auto", backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, maxWidth: 300, minWidth: 300, borderRadius: theme.palette?.borderRadius, @@ -17807,6 +18970,10 @@ const AngularWorkflow = (defaultprops) => { value={workflow.org_id} disabled={savingState !== 0 || suborgWorkflows?.length === 0 || allTriggers === undefined} onChange={(e) => { + if (cy !== undefined && cy !== null) { + cy.nodes().unselect() + } + if (lastSaved === false && originalWorkflow.id === workflow.id) { setSuborgWorkflows([]) @@ -18145,7 +19312,7 @@ const AngularWorkflow = (defaultprops) => { Runtime Location @@ -18153,6 +19320,12 @@ const AngularWorkflow = (defaultprops) => { labelId="execution_location" disabled={savingState !== 0} MenuProps={{ + PaperProps: { + sx: { + '& .MuiList-root': { + backgroundColor: "transparent", + }, + }} }} value={ selectedActionEnvironment === undefined || selectedActionEnvironment === null || selectedActionEnvironment.Name === undefined || selectedActionEnvironment.Name === null ? isCloud ? "Cloud" : "Shuffle" : selectedActionEnvironment.Name @@ -18185,13 +19358,13 @@ const AngularWorkflow = (defaultprops) => { }} style={{ pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, maxWidth: 250, minWidth: 250, borderRadius: theme.palette?.borderRadius, marginLeft: 35, - backgroundColor: theme.palette.inputColor, + backgroundColor: "transparent", height: 40, }} > @@ -18205,9 +19378,9 @@ const AngularWorkflow = (defaultprops) => { return ( @@ -18252,7 +19425,7 @@ const AngularWorkflow = (defaultprops) => { {data.default === true ? { }} >

This menu is used to control the workflow itself.

Skip Notifications
} + style={{ marginBottom: 15, color: theme.palette.text.primary }} + label={
Skip Notifications
} control={ { } /> Exit on Error
} + style={{ marginBottom: 15, color: theme.palette.text.primary }} + label={
Exit on Error
} control={ { } /> Start from top
} + style={{ marginBottom: 15, color: theme.palette.text.primary }} + label={
Start from top
} control={ { const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
{ > { e.preventDefault(); @@ -18604,17 +19782,17 @@ const AngularWorkflow = (defaultprops) => { setShowErrors(false) }} > - + - + {/**/} {workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""} {workflow.errors.slice(0, 3).map((error, index) => { // Loop through each word, and if it matches "Action " then replace it with a link to the action @@ -18718,7 +19896,6 @@ const AngularWorkflow = (defaultprops) => { if (!workflow.public && executionModalOpen) { setExecutionRunning(false); stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; const newitem = removeParam("execution_id", cursearch); navigate(curpath + newitem) setExecutionModalView(0); @@ -18771,7 +19948,6 @@ const AngularWorkflow = (defaultprops) => { if (!workflow.public && executionModalOpen) { setExecutionRunning(false); stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; const newitem = removeParam("execution_id", cursearch); navigate(curpath + newitem) setExecutionModalView(0); @@ -18938,7 +20114,7 @@ const AngularWorkflow = (defaultprops) => { .catch((error) => { console.log("Dupe workflow for suborg error: ", error.toString()) }) - } + } const BottomCytoscapeBar = () => { if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) { @@ -18969,7 +20145,12 @@ const AngularWorkflow = (defaultprops) => { workflow.public || executionRequestStarted } - style={{ height: boxSize, width: boxSize + 5, backgroundColor: green, }} + style={{ + height: boxSize, + width: boxSize + 5, + backgroundColor: green, + color: theme.palette.text.primary, + }} color="primary" variant="contained" onClick={() => { @@ -19000,7 +20181,7 @@ const AngularWorkflow = (defaultprops) => { style={{ borderTop: "1px solid rgba(74, 74, 74, 1)", borderBottom: "1px solid rgba(74, 74, 74, 1)", - height: buttonHeights, + height: buttonHeights+2, }} > {executionButton} @@ -19039,6 +20220,10 @@ const AngularWorkflow = (defaultprops) => { height: buttonHeights + 2, marginTop: -1, border: "none", + + // Remove left side borderRadius + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, } }} disabled={workflow.public} @@ -19060,7 +20245,7 @@ const AngularWorkflow = (defaultprops) => { style={{ marginLeft: 25, marginTop: 2, - border: "1px solid rgba(255,255,255,0.3)", + border: theme.palette.DialogStyle.border, borderRadius: theme.palette?.borderRadius / 2, maxHeight: buttonHeights, @@ -19075,6 +20260,10 @@ const AngularWorkflow = (defaultprops) => { style={{ height: buttonHeights, width: 64, + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: 0, }} variant={ lastSaved && !workflow.public ? "text" : "contained" @@ -19092,7 +20281,7 @@ const AngularWorkflow = (defaultprops) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -19114,7 +20303,7 @@ const AngularWorkflow = (defaultprops) => { }} > {savingState === 2 ? ( - + ) : savingState === 1 ? ( ) : ( @@ -19278,6 +20467,29 @@ const AngularWorkflow = (defaultprops) => { + + + {userdata?.support === true ? + + + + + + : null} @@ -19286,12 +20498,115 @@ const AngularWorkflow = (defaultprops) => { ); }; + const setupResizeHandlers = (cy, nodeId) => { + let height; + let width; + let resizeTimeout; + + cy.on("drag", ".resize-handle", (event) => { + if (resizeTimeout) return; + + resizeTimeout = setTimeout(() => { + resizeTimeout = null; + const handle = event.target; + const parent = cy.$(`#${nodeId}`); // Fetch the main node directly + + // Check if the parent node exists + if (!parent || parent.empty()) { + console.warn(`Parent node (${nodeId}) not found.`); + return; + } + + if (!handle?.position()) { + console.warn(`Handle position is undefined for ${handle.id()}`); + return; + } + + const handlePos = handle.position(); + const parentPos = parent.position(); + + if (!parentPos) { + console.warn(`Parent position is undefined for ${nodeId}`); + return; + } + + // Calculate new width & height based on handle movement + const newWidth = Math.abs(handlePos.x - parentPos.x) * 2; + const newHeight = Math.abs(handlePos.y - parentPos.y) * 2; + + // Apply min/max constraints + const constrainedWidth = Math.max(100, Math.min(newWidth, 500)); + const constrainedHeight = Math.max(50, Math.min(newHeight, 300)); + + // Update node size + parent.style({ + width: constrainedWidth, + height: constrainedHeight, + }); + + // Store dimensions for state update on drag end + height = Math.floor(constrainedHeight); + width = Math.floor(constrainedWidth); + + // Update handle positions + cy.$(".resize-handle").forEach((corner) => { + if (!corner?.id() || !corner.position()) return; + + const { x, y } = parent.position(); + const offsetX = corner.id().includes("left") ? -constrainedWidth / 2 : constrainedWidth / 2; + const offsetY = corner.id().includes("top") ? -constrainedHeight / 2 : constrainedHeight / 2; + + corner.position({ x: x + offsetX, y: y + offsetY }); + }); + }, 16); // Throttle to ~60 FPS + }); + + // Update state when resizing ends + cy.on("free", ".resize-handle", (event) => { + const data = event.target.data(); + const parentNode = cy.getElementById(data.attachedTo); + + if (parentNode) { + + parentNode.data({ + ...parentNode.data(), + width: Math.floor(width), + height: Math.floor(height), + }); + setSelectedComment((prev) => ({ + ...prev, + width: Math.floor(width), + height: Math.floor(height), + })); + } + }); + + }; + + + const setupNodeDragHandler = (cy, nodeId) => { + cy.on("drag", `#${nodeId}`, (event) => { + const node = event.target; + const { x, y } = node.position(); + const width = parseFloat(node.style("width")); + const height = parseFloat(node.style("height")); + + // Move resize handles with the node + cy.$(".resize-handle").forEach((corner) => { + const offsetX = corner.id().includes("left") ? -width / 2 : width / 2; + const offsetY = corner.id().includes("top") ? -height / 2 : height / 2; + + corner.position({ x: x + offsetX, y: y + offsetY }); + }); + }); + }; + const addCommentNode = () => { const newId = uuidv4(); - const position = { - x: 300, - y: 300, - }; + const position = { x: 300, y: 300 }; + const width = 250; + const height = 150; + const handleOffset = 10; // Move handles outside the node cy.add({ group: "nodes", @@ -19301,20 +20616,52 @@ const AngularWorkflow = (defaultprops) => { type: "COMMENT", is_valid: true, decorator: true, - width: 250, - height: 150, - position: position, + width, + height, + position, backgroundcolor: "#1f2023", color: "#ffffff", + + textHalign: "right", + textValign: "bottom", + textMarginX: "-250px", + textMarginY: "-150px", }, - position: position, + position, }); + + // Define corner positions relative to the main node + const corners = [ + { id: `${newId}-top-left`, dx: -width / 2 - handleOffset, dy: -height / 2 - handleOffset }, + { id: `${newId}-top-right`, dx: width / 2 + handleOffset, dy: -height / 2 - handleOffset }, + { id: `${newId}-bottom-left`, dx: -width / 2 - handleOffset, dy: height / 2 + handleOffset }, + { id: `${newId}-bottom-right`, dx: width / 2 + handleOffset, dy: height / 2 + handleOffset }, + ]; + + // Add resize handles **without** the parent property + corners.forEach((corner) => { + cy.add({ + group: "nodes", + data: { + id: corner.id, + type: "RESIZE-HANDLE", + is_valid: true, + attachedTo: newId, + decorator: true, + }, // No parent to avoid edges + position: { x: position.x + corner.dx, y: position.y + corner.dy }, + classes: "resize-handle", + }); + }); + + setupResizeHandlers(cy, newId); + setupNodeDragHandler(cy, newId); }; const RightSideBar = (props) => { var defaultReturn = null - if (Object.getOwnPropertyNames(selectedComment).length > 0) { + if (Object.getOwnPropertyNames(selectedComment).length > 0 && selectedComment.hasOwnProperty('id')) { defaultReturn = } else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) { if (selectedTrigger.trigger_type === undefined) { @@ -19484,7 +20831,7 @@ const AngularWorkflow = (defaultprops) => { return ( { @@ -19772,10 +21119,10 @@ const AngularWorkflow = (defaultprops) => { minWidth: "95%", maxWidth: "95%", marginTop: 5, - color: "white", + color: theme.palette.text.primary, marginBottom: 10, padding: 5, - backgroundColor: theme.palette.backgroundColor, + backgroundColor: theme.palette.platformColor, borderRadius: theme.palette.borderRadius, cursor: "pointer", display: "flex", @@ -19807,6 +21154,7 @@ const AngularWorkflow = (defaultprops) => { height: 30, paddingLeft: 0, width: 30, + color: theme.palette.text.primary, }} onClick={() => { if (validate.valid) { @@ -19836,18 +21184,17 @@ const AngularWorkflow = (defaultprops) => { > - + { return collapseField(jsonField) }} @@ -20033,127 +21380,6 @@ const AngularWorkflow = (defaultprops) => { ); }; - const handleReactJsonClipboard = (copy) => { - - const elementName = "copy_element_shuffle"; - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - if ( - copy.namespace !== undefined && - copy.name !== undefined && - copy.src !== undefined - ) { - copy = copy.src; - } - - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } - - var stringified = JSON.stringify(copy); - if (stringified.startsWith('"') && stringified.endsWith('"')) { - stringified = stringified.substring(1, stringified.length - 1); - } - - navigator.clipboard.writeText(stringified); - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - - console.log("COPYING!"); - toast("Copied value to clipboard, NOT json path.") - } else { - console.log("Failed to copy from " + elementName + ": ", copyText); - } - }; - - const HandleJsonCopy = (base, copy, base_node_name) => { - if (typeof copy.name === "string") { - copy.name = copy.name.replaceAll(" ", "_"); - } - - //lol - if (typeof base === 'object' || typeof base === 'dict') { - base = JSON.stringify(base) - } - - if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") { - base_node_name = "exec" - } - - //console.log("COPY: ", base_node_name, copy); - - //var newitem = JSON.parse(base); - var newitem = validateJson(base).result - - // Check if base_node_name has changed - if (cy !== undefined && cy !== null) { - //console.log("Change name?") - //const allNodes = cy.nodes().jsons(); - //for (var key in allNodes) { - //const currentNode = allNodes[key]; - - //if (currentNode. - //} - - //const nodedata = cy.getElementById(data.action.id).data(); - //base_node_name = - } - - to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_"); - for (let copykey in copy.namespace) { - if (copy.namespace[copykey].includes("Results for")) { - continue; - } - - if (newitem !== undefined && newitem !== null) { - newitem = newitem[copy.namespace[copykey]]; - if (!isNaN(copy.namespace[copykey])) { - to_be_copied += ".#"; - } else { - to_be_copied += "." + copy.namespace[copykey]; - } - } - } - - if (newitem !== undefined && newitem !== null) { - newitem = newitem[copy.name]; - if (!isNaN(copy.name)) { - to_be_copied += ".#"; - } else { - to_be_copied += "." + copy.name; - } - } - - to_be_copied = to_be_copied.replaceAll(" ", "_"); - console.log("COPY: ", to_be_copied); - const elementName = "copy_element_shuffle"; - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - //console.log("NAVIGATOR: ", navigator); - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } - - navigator.clipboard.writeText(to_be_copied); - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - //console.log("COPYING!"); - toast("Copied JSON path to clipboard.") - } else { - console.log("Couldn't find element ", elementName); - } - } - // Not used because of issue with state updates. const ShowReactJsonField = (props) => { const { validate, jsonValue, collapsed, label, autocomplete } = props @@ -20191,7 +21417,6 @@ const AngularWorkflow = (defaultprops) => { src={validate.result} theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} - collapsed={parsedCollapse} shouldCollapse={(jsonField) => { return collapseField(jsonField) }} @@ -20344,6 +21569,7 @@ const AngularWorkflow = (defaultprops) => { const envStatus = !(executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0) ? "loading" : "success" var executionDelay = -75 + const executionModal = ( { onClose={() => { setExecutionModalOpen(false) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) + //const newitem = removeParam("execution_id", cursearch); + //navigate(curpath + newitem) }} style={{ resize: "both", @@ -20372,12 +21597,12 @@ const AngularWorkflow = (defaultprops) => { overflow: "auto", minWidth: isMobile ? "100%" : 490, maxWidth: isMobile ? "100%" : 490, - color: "white", + color: theme.palette.text.primary, fontSize: 18, borderLeft: theme.palette.defaultBorder, borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + backgroundColor: themeMode === "dark" ? "black" : theme.palette.drawer.backgroundColor, }, }} > @@ -20394,19 +21619,19 @@ const AngularWorkflow = (defaultprops) => { setExecutionModalOpen(false) }} > - + : null} {executionModalView === 0 ? ( -
+
- - - + - > +
) : null} @@ -20688,11 +21911,11 @@ const AngularWorkflow = (defaultprops) => { {foundnotifications > 0 ? { e.preventDefault() e.stopPropagation() - window.open(`/admin?admin_tab=notifications&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") + window.open(`/admin?org_id=${workflow.org_id}&admin_tab=notifications&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") }} /> @@ -20722,19 +21945,24 @@ const AngularWorkflow = (defaultprops) => {
) : ( -
- +
+ No executions found for the '{executionFilter}' filter.
) : ( -
+
{ stop() }} > - + -

{ - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; const newitem = removeParam("execution_id", cursearch); navigate(curpath + newitem) setExecutionRunning(false); stop() }} > - See more runs + Back to all runs

-
-
+

Details

+ Rerun workflow. Uses same startnode as the original. Runs from scratch. + + } + placement="left" style={{ zIndex: 50000 }} > @@ -20880,7 +22109,7 @@ const AngularWorkflow = (defaultprops) => { @@ -20917,7 +22146,7 @@ const AngularWorkflow = (defaultprops) => {
+ + {data.action.app_name === "AI Agent" || data.action.app_name === "Shuffle Agent" ? + + + { }} + > + + + + + : null} + {data.action.app_name === "shuffle-subflow" && validate.result.success !== undefined && validate.result.success === true ? ( @@ -21535,7 +22813,7 @@ const AngularWorkflow = (defaultprops) => { target="_blank" style={{ textDecoration: "none", - color: "#FF8544", + color: theme.palette.linkColor, }} onClick={(event) => { }} > @@ -21567,7 +22845,6 @@ const AngularWorkflow = (defaultprops) => { src={validate.result} theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} - collapsed={false} shouldCollapse={(jsonField) => { return collapseField(jsonField) }} @@ -21654,6 +22931,11 @@ const AngularWorkflow = (defaultprops) => { const checked = validateJson(data.value.trim()) if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { + + if (isCloud) { + return null + } + return (
{ Action Logs - More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. + More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.
) @@ -21729,7 +23011,6 @@ const AngularWorkflow = (defaultprops) => { src={checked.result} theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} - collapsed={data.value.length < 10000 ? false : true} shouldCollapse={(jsonField) => { return collapseField(jsonField) }} @@ -21744,7 +23025,7 @@ const AngularWorkflow = (defaultprops) => { variant="body2" style={{ whiteSpace: 'pre-line', - color: showlink ? "#FF8544" : "white", + color: showlink ? "#FF8544" : theme.palette.text.primary, cursor: showlink ? "pointer" : "default", }} onClick={(e) => { @@ -21799,7 +23080,7 @@ const AngularWorkflow = (defaultprops) => { } if (result.status === 405) { - return "Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to support@shuffler.io" + return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}` } if (result.status === 415) { @@ -21866,7 +23147,7 @@ const AngularWorkflow = (defaultprops) => { } if (stringjson.includes("kms/")) { - return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io" + return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}` } if (stringjson.includes("invalidurl")) { @@ -21894,7 +23175,7 @@ const AngularWorkflow = (defaultprops) => { if (stringjson.includes("connectionerror")) { if (stringjson.includes("kms")) { - return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io" + return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}` } return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." @@ -21915,23 +23196,25 @@ const AngularWorkflow = (defaultprops) => { open={codeModalOpen} PaperProps={{ onClick: () => setActiveDialog("result"), - style: { + sx: { pointerEvents: "auto", - color: "white", - minWidth: isMobile ? "90%" : 750, - padding: 30, - maxHeight: 550, + color: theme.palette.text.primary, + minWidth: isMobile ? "90%" : "750px", + maxHeight: "550px", overflowY: "auto", overflowX: "hidden", border: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.DialogStyle.borderRadius, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + padding: "30px 30px 30px 30px", + }, }, }} > - {/* Have a sticky top bar */} - + { } }} > - + { } }} > - + { } }} > - + { onClick={(e) => { }} > - + { setCodeModalOpen(false); }} > - + +
{curapp === null ? null : ( {selectedResult.action.app_name} { fontSize: 24, marginTop: "auto", marginBottom: "auto", - cursor: "move", }} > {selectedResult.action.label.replaceAll("_", " ")}
-
{selectedResult.action.name}
+
{selectedResult.action.name}
@@ -22155,7 +23438,6 @@ const AngularWorkflow = (defaultprops) => { src={validate.result} theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} - collapsed={selectedResult.result.length < 10000 ? false : true} shouldCollapse={(jsonField) => { return collapseField(jsonField) }} @@ -22216,6 +23498,7 @@ const AngularWorkflow = (defaultprops) => {
)} +
{selectedResult.action.parameters !== null && selectedResult.action.parameters !== undefined ? ( @@ -22254,11 +23537,12 @@ const AngularWorkflow = (defaultprops) => { ) : null}
+ ); const newView = ( -
+
@@ -22388,6 +23672,7 @@ const AngularWorkflow = (defaultprops) => { suborgWorkflows={suborgWorkflows} originalWorkflow={originalWorkflow} + runFromHere={runFromHere} />
@@ -22498,7 +23783,7 @@ const AngularWorkflow = (defaultprops) => { PaperProps={{ style: { pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : 800, minWidth: isMobile ? bodyWidth - 100 : 800, @@ -22510,7 +23795,7 @@ const AngularWorkflow = (defaultprops) => { > - Runtime Variable + Runtime Variable Runtime Variables are TEMPORARY variables that you can only be set @@ -22519,7 +23804,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > here @@ -22530,7 +23815,7 @@ const AngularWorkflow = (defaultprops) => { style={{ marginTop: 25 }} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} margin="dense" @@ -22545,7 +23830,7 @@ const AngularWorkflow = (defaultprops) => { style={{ marginTop: 25 }} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} margin="dense" @@ -22613,7 +23898,7 @@ const AngularWorkflow = (defaultprops) => { { PaperProps={{ style: { pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : "100%", @@ -22688,7 +23973,7 @@ const AngularWorkflow = (defaultprops) => { > - Workflow Variable + Workflow Variable { placeholder="Name" InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} margin="dense" @@ -22712,7 +23997,7 @@ const AngularWorkflow = (defaultprops) => { fullWidth InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} defaultValue={newVariableDescription} @@ -22726,7 +24011,7 @@ const AngularWorkflow = (defaultprops) => { margin="dense" InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} fullWidth @@ -22875,11 +24160,8 @@ const AngularWorkflow = (defaultprops) => { // Automatically mapping fields that already exist (predefined). // Warning if fields are NOT filled for (let paramkey in selectedApp.authentication.parameters) { - if ( - authenticationOption.fields[ - selectedApp.authentication.parameters[paramkey].name - ].length === 0 - ) { + if (authenticationOption.fields[selectedApp.authentication.parameters[paramkey].name].length === 0) { + if ( selectedApp.authentication.parameters[paramkey].value !== undefined && selectedApp.authentication.parameters[paramkey].value !== null && @@ -22928,8 +24210,30 @@ const AngularWorkflow = (defaultprops) => { var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); var newFields = []; + + var warningsent = false for (let authkey in newAuthOption.fields) { - const value = newAuthOption.fields[authkey]; + var value = newAuthOption.fields[authkey]; + + if (value?.toLowerCase().includes("secret. replace")) { + value = "" + + if (authkey === "url") { + // Use default value of the url + const urlparam = selectedApp.authentication.parameters.find((data) => data.name === "url") + if (urlparam !== undefined && urlparam !== null) { + if (urlparam.example !== undefined && urlparam.example !== null && urlparam.example.length > 0) { + value = urlparam.example + } + } + } else { + if (!warningsent) { + warningsent = true + toast("Warning: As you didn't fill in all fields, be aware that the authentication may fail.") + } + } + } + newFields.push({ "key": authkey, "value": value, @@ -22953,7 +24257,7 @@ const AngularWorkflow = (defaultprops) => { return (
-
+
Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
@@ -22962,7 +24266,7 @@ const AngularWorkflow = (defaultprops) => { target="_blank" rel="noopener noreferrer" href="https://shuffler.io/docs/apps#authentication" - style={{ textDecoration: "none", color: "#FF8544" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > What is app authentication? @@ -23045,7 +24349,7 @@ const AngularWorkflow = (defaultprops) => { }} style={{ backgroundColor: theme.palette.surfaceColor, - color: "white", + color: theme.palette.text.primary, height: 50, }} > @@ -23053,7 +24357,7 @@ const AngularWorkflow = (defaultprops) => { key={"false"} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }} value={"false"} > @@ -23063,7 +24367,7 @@ const AngularWorkflow = (defaultprops) => { key={"true"} style={{ backgroundColor: theme.palette.inputColor, - color: "white", + color: theme.palette.text.primary, }} value={"true"} > @@ -23082,18 +24386,18 @@ const AngularWorkflow = (defaultprops) => { }} fullWidth type={ - data.example !== undefined && data.example.includes("***") + data.example !== undefined && data.example.includes("**") ? "password" : "text" } color="primary" defaultValue={ - data.value !== undefined && data.value !== null && !data.value.includes("Secret. Replace") ? data.value : "" + data.value !== undefined && data.value !== null && !data.value.includes("Secret. Replace") ? data.value : + data?.example !== undefined && data?.example !== null && data?.example !== "" && !data?.example.includes("*") ? data.example : "" } placeholder={data.example} onChange={(event) => { - authenticationOption.fields[data.name] = - event.target.value; + authenticationOption.fields[data.name] = event.target.value; }} id={`${data.name}_auth`} /> @@ -23127,7 +24431,7 @@ const AngularWorkflow = (defaultprops) => { open={configureWorkflowModalOpen} PaperProps={{ style: { - color: "white", + color: theme.palette.text.primary, minWidth: 650, border: theme.palette.defaultBorder, @@ -23142,7 +24446,7 @@ const AngularWorkflow = (defaultprops) => { position: "absolute", top: 14, right: 14, - color: "white", + color: theme.palette.text.primary, }} onClick={() => { setConfigureWorkflowModalOpen(false); @@ -23184,25 +24488,35 @@ const AngularWorkflow = (defaultprops) => { hideBackdrop={true} disableEnforceFocus={true} disableBackdropClick={true} - style={{ pointerEvents: "none" }} + style={{ + pointerEvents: "none", + }} open={authenticationModalOpen} onClose={() => { setSelectedMeta(undefined) }} PaperProps={{ - style: { + sx: { pointerEvents: "auto", - color: "white", - minWidth: 1100, - minHeight: 700, - maxHeight: 700, - padding: 15, + color: theme.palette.DialogStyle.color, + minWidth: "1100px", + minHeight: "700px", + maxHeight: "700px", overflow: "hidden", zIndex: 10012, - border: theme.palette.defaultBorder, + border: theme.palette.DialogStyle.border, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.DialogStyle.borderRadius, + backgroundColor: theme.palette.DialogStyle.backgroundColor, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, }, }} > @@ -23214,6 +24528,7 @@ const AngularWorkflow = (defaultprops) => { right: 75, height: 50, width: 50, + backgroundColor: theme.palette.DialogStyle.backgroundColor, }} > {selectedApp.reference_info === undefined || @@ -23226,7 +24541,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href={"https://github.com/shuffle/python-apps"} - style={{ textDecoration: "none", color: "#f86a3e" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > {`Documentation { rel="noopener noreferrer" target="_blank" href={selectedApp.reference_info.github_url} - style={{ textDecoration: "none", color: "#f86a3e" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > {`Documentation { > -
+
{ selectedApp.authentication.parameters === null || selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0 ? 0 : 2, - padding: 0, + paddingLeft: 15, + paddingTop: 15, + paddingBottom: 15, minHeight: isMobile ? "90%" : 700, maxHeight: isMobile ? "90%" : 700, overflowY: "auto", overflowX: isMobile ? "auto" : "hidden", + backgroundColor: theme.palette.DialogStyle.backgroundColor, }} > {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? @@ -23346,6 +24664,7 @@ const AngularWorkflow = (defaultprops) => { minHeight: 630, overflowY: "auto", overflowX: "hidden", + backgroundColor: theme.palette.DialogStyle.backgroundColor, }} onLoad={() => { /* @@ -23433,7 +24752,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href="https://discord.gg/B2CBzUm" - style={{ textDecoration: "none", color: "#f86a3e" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > Join the community on Discord! @@ -23453,7 +24772,7 @@ const AngularWorkflow = (defaultprops) => { rel="noopener noreferrer" target="_blank" href={"https://github.com/shuffle/python-apps"} - style={{ textDecoration: "none", color: "#f86a3e" }} + style={{ textDecoration: "none", color: theme.palette.linkColor }} > Check it out on Github! @@ -23495,7 +24814,7 @@ const AngularWorkflow = (defaultprops) => { href={selectedMeta.link} style={{ textDecoration: "none", color: "#FF8544" }} > - @@ -23506,13 +24825,13 @@ const AngularWorkflow = (defaultprops) => { style={{ height: "100%", width: 1, - backgroundColor: "white", + backgroundColor: theme.palette.text.primary, marginLeft: 50, marginRight: 50, }} /> )} - + {selectedMeta.read_time} minute {selectedMeta.read_time === 1 ? "" : "s"} to read @@ -23592,7 +24911,7 @@ const AngularWorkflow = (defaultprops) => { PaperProps={{ style: { pointerEvents: "auto", - color: "white", + color: theme.palette.text.primary, minWidth: 600, minHeight: 250, maxHeight: 250, @@ -23607,7 +24926,7 @@ const AngularWorkflow = (defaultprops) => { }} > -
Run a Tenzir Pipeline
+
Run a Tenzir Pipeline
Runs a Tenzir pipeline. You can use the output of the pipeline in your workflow. @@ -23659,7 +24978,7 @@ const AngularWorkflow = (defaultprops) => { return (
{/* - + */} { }); }} > - +
{ @@ -24030,7 +25349,7 @@ const AngularWorkflow = (defaultprops) => { minWidth: isMobile ? "100%" : 360, maxWidth: isMobile ? "100%" : 360, backgroundColor: theme.palette.platformColor, - color: "white", + color: theme.palette.text.primary, fontSize: 18, zIndex: 15001, borderRight: theme.palette.defaultBorder, @@ -24060,7 +25379,7 @@ const AngularWorkflow = (defaultprops) => { : null*/}
-
+
{selectedVersion?.name} @@ -24262,6 +25581,8 @@ const AngularWorkflow = (defaultprops) => { } const handleActionParamChange = (actionId, fieldName, newData) => { + var updateFail = "" + if (workflow !== undefined) { // Find the action with matching id const actionIndex = workflow?.actions.findIndex(action => action.id === actionId); @@ -24276,9 +25597,20 @@ const AngularWorkflow = (defaultprops) => { // Update workflow state to trigger re-render setWorkflow({...workflow}); setLastSaved(false); - } - } - } + setUpdate(Math.random()) + } else { + updateFail = "Parameter is undefined or null" + } + } else { + updateFail = "Trigger is undefined or null" + } + } else { + updateFail = "Workflow is undefined or null" + } + + if (updateFail !== "") { + toast.error(updateFail + " - Failed to update subflow parameter value. Please try again.") + } } /* var foundusecase = {} @@ -24369,7 +25701,7 @@ const AngularWorkflow = (defaultprops) => { // selectedTrigger={selectedTrigger} aiSubmit={aiSubmit} toolsAppId={toolsApp.id} - handleSubflowParamChange={handleSubflowParamChange} + handleTriggerParamChange={handleTriggerParamChange} codedata={editorData.value} setcodedata={setcodedata} selectedEdge={selectedEdge} @@ -24391,6 +25723,7 @@ const AngularWorkflow = (defaultprops) => { setAiQueryModalOpen={setAiQueryModalOpen} + isWorkflowEditor={editorData?.name === "workflow yaml"} /> : null} @@ -24417,7 +25750,7 @@ const AngularWorkflow = (defaultprops) => { position: "absolute", top: 6, right: 6, - color: "white", + color: theme.palette.text.primary, }} onClick={() => { setSelectionOpen(false) @@ -24451,7 +25784,7 @@ const AngularWorkflow = (defaultprops) => { position: "absolute", top: 6, right: 6, - color: "white", + color: theme.palette.text.primary, }} onClick={() => { setShowVideo("") diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index 96298437..d104d43d 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -50,7 +50,7 @@ import { green } from "../views/AngularWorkflow.jsx" const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ) // Lazy loading of ApiExplorer component to reduce initial load time @@ -59,6 +59,7 @@ const ApiExplorer = React.lazy(() => import("../components/ApiExplorer.jsx")); const ApiExplorerWrapper = (props) => { const { globalUrl, serverside, userdata, isLoggedIn, isLoaded} = props; + const { supportEmail } = useContext(Context); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const location = useLocation(); const navigate = useNavigate(); @@ -165,7 +166,7 @@ const ApiExplorerWrapper = (props) => { } if (!found) { - toast.error(`Failed to get API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, { + toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, { "autoClose": 10000, }) @@ -174,7 +175,7 @@ const ApiExplorerWrapper = (props) => { },3000) } } else { - toast.error(`Failed to get API data for '${appname}' (2). Contact support@shuffler.io if this persists.`, { + toast.error(`Failed to get API data for '${appname}' (2). Contact ${supportEmail} if this persists.`, { "autoClose": 10000, }) setTimeout(()=>{ @@ -539,7 +540,7 @@ const ApiExplorerWrapper = (props) => { }else if (openapi?.id?.length > 0) { appid = openapi?.id; }else{ - toast.error("App id is missing and we can't run the API. Please contact support@shuffler.io if this persists."); + toast.error(`App id is missing and we can't run the API. Please contact ${supportEmail} if this persists.`); return; } @@ -673,7 +674,7 @@ const ApiExplorerWrapper = (props) => { if (data.result.includes("custom_action doesn't exist")) { // No timeout error - toast.info("This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to support@shuffler.io", { + toast.info(`This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to ${supportEmail}`, { "autoClose": 90000, }) } else if (data.result.includes("authentication") && data.result.includes("Oauth2")) { diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 2d1bbb32..1eaf5fcb 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { makeStyles } from "@mui/styles"; import { BrowserView, MobileView } from "react-device-detect"; -import theme from '../theme.jsx'; +import { getTheme } from '../theme.jsx'; import { Paper, @@ -57,6 +57,7 @@ import { ToastContainer, toast } from "react-toastify" import words from "shellwords"; import AvatarEditor from "react-avatar-editor"; +import { Context } from "../context/ContextApi.jsx"; const surfaceColor = "#27292D"; const inputColor = "#383B40"; @@ -67,33 +68,7 @@ const bodyDivStyle = { zoom: 0.8, }; -const actionListStyle = { - paddingLeft: "10px", - paddingRight: "10px", - paddingBottom: "10px", - paddingTop: "10px", - marginTop: "5px", - display: "flex", - color: "white", - position: "relative", - backgroundColor: theme.palette.platformColor, -}; - -const boxStyle = { - color: "white", - flex: "1", - marginLeft: "10px", - marginRight: "10px", - paddingLeft: "30px", - paddingRight: "30px", - paddingBottom: "30px", - paddingTop: "30px", - display: "flex", - flexDirection: "column", - - backgroundColor: theme.palette.backgroundColor, -}; const dividerStyle = { marginBottom: "10px", @@ -427,9 +402,40 @@ const AppCreator = (defaultprops) => { const [actionAmount, setActionAmount] = useState(increaseAmount); const [newAppGroup, setNewAppGroup] = useState("") + const { themeMode, supportEmail, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const [oauth2Scopes, setOauth2Scopes] = useState([]); const [oauth2Type, setOauth2Type] = useState("delegated"); + const actionListStyle = { + paddingLeft: "10px", + paddingRight: "10px", + paddingBottom: "10px", + paddingTop: "10px", + marginTop: "5px", + display: "flex", + color: theme.palette.text.primary, + position: "relative", + + backgroundColor: theme.palette.platformColor, + }; + + const boxStyle = { + color: theme.palette.text.primary, + flex: "1", + marginLeft: "10px", + marginRight: "10px", + paddingLeft: "30px", + paddingRight: "30px", + paddingBottom: "30px", + paddingTop: "30px", + display: "flex", + flexDirection: "column", + + backgroundColor: theme.palette.platformColor, + }; + //client_credentials const [oauth2GrantType, setOauth2GrantType] = useState(""); const defaultAuth = { @@ -739,7 +745,7 @@ const AppCreator = (defaultprops) => { } setBasedata(data); - console.log("Info: ", data) + console.log("Loaded Info: ", data) try { if (data.info !== null && data.info !== undefined) { @@ -753,7 +759,12 @@ const AppCreator = (defaultprops) => { if (data.info.title.length > 29) { setName(data.info.title.slice(0, 29)); } else { - setName(data.info.title); + // Check if fork=true in URL + if (window.location.search.includes("fork=true")) { + setName(data.info.title + " Fork"); + } else { + setName(data.info.title) + } } } @@ -943,8 +954,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 +967,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'), "") } @@ -1968,7 +1978,7 @@ const AppCreator = (defaultprops) => { // Saving the app that's been configured. // Save SAVE app const submitApp = () => { - toast("Uploading and building app " + name); + toast.info("Uploading and building app " + name + ". This may take a minute or two."); setAppBuilding(true); setErrorCode(""); @@ -1978,6 +1988,8 @@ const AppCreator = (defaultprops) => { const schemes = [splitBase[0]]; const basePath = "/" + splitBase.slice(3).join("/"); + const newBaseUrl = baseUrl.replaceAll("http//", "http://").replaceAll("https//", "https://") + const data = { openapi: "3.0.0", info: { @@ -1986,7 +1998,7 @@ const AppCreator = (defaultprops) => { version: "1.0", "x-logo": fileBase64, }, - servers: [{ url: baseUrl }], + servers: [{ url: newBaseUrl }], host: host, basePath: basePath, schemes: schemes, @@ -2037,7 +2049,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) { @@ -2595,15 +2607,23 @@ const AppCreator = (defaultprops) => { } if (setExtraAuth.length > 0) { + const invalidfieldnames = ["apikey", "username", "password", "username_basic", "password_basic", "url", "access_token"] for (let authkey in extraAuth) { const curauth = extraAuth[authkey]; - if (curauth.name.length === 0 || curauth.name.toLowerCase() == "url") { toast("Can't add extra auth with empty name or Name URL"); setAppBuilding(false); return; } + // Additional comparisonchecks + if (authenticationOption !== "No authentication" && authenticationOption !== "") { + if (invalidfieldnames.includes(curauth.name.toLowerCase())) { + toast.warn("Skipping extra auth field: " + curauth.name + ". This is not valid.") + continue + } + } + data.components.securitySchemes[curauth.name] = { type: "apiKey", in: curauth.type, @@ -2624,12 +2644,17 @@ const AppCreator = (defaultprops) => { credentials: "include", }) .then((response) => { + setAppBuilding(false) if (response.status === 403) { + + var urlParams = new URLSearchParams(window.location.search) if (urlParams.has("id")) { - toast.error("Please log in to build this app. If this error persists, please contact support@shuffler.io") + toast.error(`Please log in to build this app. If this error persists, please contact ${supportEmail}`) } else { - toast.error("Failed to save the app as you are not the owner. Redirecting you to the forking page. When there, save again.") + toast.error("Failed to save the app as you are not the owner. Redirecting you to the forking page. If this does not load, please download and re-import the app.", { + autoClose: 10000 + }) if (props.match.params.appid !== undefined && props.match.params.appid !== null && props.match.params.appid.length > 0) { setTimeout(() => { window.open(`/apps/new?id=${props.match.params.appid}`, "_blank") @@ -2645,24 +2670,28 @@ const AppCreator = (defaultprops) => { //throw new Error("NOT 200 :O") } - setAppBuilding(false); return response.json(); }) .then((responseJson) => { - if (!responseJson.success) { - if (responseJson.extra !== undefined && responseJson.extra !== null) { + if (responseJson?.success !== true) { + if (responseJson?.extra !== undefined && responseJson?.extra !== null) { toast("Failed building: " + responseJson.extra); - } - - if (responseJson.reason !== undefined) { + } else if (responseJson?.reason !== undefined) { setErrorCode(responseJson.reason); - if (responseJson.extra === undefined && responseJson.extra === null) { - toast("Failed to verify: " + responseJson.reason); + if (responseJson?.details !== undefined && responseJson?.details !== null) { + toast.error("Failed to build - contact support@shuffler.io: " + responseJson.details, { + autoClose: 60000 + }) + } else { + toast.error("Failed to build: " + responseJson.reason, { + autoClose: 10000 + }) } - } + } else { + } } else { - toast("Successfully uploaded openapi"); + toast.success("Successfully built openapi app! Added job to rebuild it in your hybrid runtime locations (Orborus)."); if (window.location.pathname.includes("/new")) { if (responseJson.id !== undefined && responseJson.id !== null) { window.location = `/apps/edit/${responseJson.id}`; @@ -2673,18 +2702,18 @@ const AppCreator = (defaultprops) => { .catch((error) => { setAppBuilding(false); setErrorCode(error.toString()); - toast(error.toString()); + toast.error(error.toString()); }); }; const bearerAuth = authenticationOption === "Bearer auth" ? ( -
+

Bearer auth @@ -2697,12 +2726,12 @@ const AppCreator = (defaultprops) => { // Basicauth const basicAuth = authenticationOption === "Basic auth" ? ( -
+

Basic authentication @@ -2796,7 +2825,7 @@ const AppCreator = (defaultprops) => { flex: 2, marginTop: 0, marginBottom: 0, - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginRight: 5, }} fullWidth={true} @@ -2805,16 +2834,28 @@ const AppCreator = (defaultprops) => { margin="normal" variant="outlined" defaultValue={extraAuth[index].name} + helperText={ + + {extraAuth[index]?.name?.toLowerCase() === "url" + || extraAuth[index]?.name?.toLowerCase() === "apikey" + ? `ERROR: Invalid key: ${extraAuth[index].name}. ` : ""} + + } onChange={(e) => { extraAuth[index].name = e.target.value; setExtraAuth(extraAuth); }} + onBlur={(e) => { + // Forcerender + setUpdate(Math.random()); + }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, minHeight: 50, marginLeft: 5, maxWidth: "95%", @@ -2829,7 +2870,7 @@ const AppCreator = (defaultprops) => { marginTop: 0, marginBottom: 0, flex: 2, - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginRight: 5, }} fullWidth={true} @@ -2844,7 +2885,8 @@ const AppCreator = (defaultprops) => { }} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, minHeight: 50, marginLeft: 5, maxWidth: "95%", @@ -2862,9 +2904,9 @@ const AppCreator = (defaultprops) => { value={extraAuth[index].type} style={{ flex: 1, - backgroundColor: inputColor, + backgroundColor: theme.palette.backgroundColor, paddingLeft: "10px", - color: "white", + color: theme.palette.text.primary, height: 50, borderRadius: theme.shape.borderRadius, }} @@ -2875,14 +2917,14 @@ const AppCreator = (defaultprops) => { > Header Query @@ -2930,7 +2972,7 @@ const AppCreator = (defaultprops) => { const jwtAuth = authenticationOption === "JWT" ? ( -
+
JWT authentication { variant="outlined" defaultValue={parameterName} helperText={ - + Must start with / and be a valid path } @@ -2959,7 +3001,7 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -2980,7 +3022,7 @@ const AppCreator = (defaultprops) => { variant="outlined" defaultValue={parameterName} helperText={ - + Must use 'key=value&key=value' format } @@ -2992,7 +3034,7 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3002,7 +3044,7 @@ const AppCreator = (defaultprops) => { const oauth2Auth = authenticationOption === "Oauth2" ? ( -
+
Oauth2 authentication { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3112,7 +3154,7 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3161,7 +3203,7 @@ const AppCreator = (defaultprops) => { }} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3178,7 +3220,7 @@ const AppCreator = (defaultprops) => { required InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, maxHeight: 160, }, }} @@ -3203,7 +3245,7 @@ const AppCreator = (defaultprops) => { const apiKey = authenticationOption === "API key" ? ( -
+
API key authentication Do NOT put your actual API-key. Add the name of the field used for authentication, e.g. "X-APIKEY". @@ -3222,7 +3264,7 @@ const AppCreator = (defaultprops) => { variant="outlined" value={parameterName} helperText={ - + Can't be empty or contain any of the following: !#$%&'^"+-._~|]+$:= } @@ -3239,7 +3281,7 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3256,7 +3298,7 @@ const AppCreator = (defaultprops) => { borderRadius: theme.shape.borderRadius, backgroundColor: inputColor, paddingLeft: 10, - color: "white", + color: theme.palette.text.primary, height: 57, }} inputProps={{ @@ -3272,7 +3314,7 @@ const AppCreator = (defaultprops) => { return ( {data} @@ -3464,15 +3506,14 @@ const AppCreator = (defaultprops) => { { @@ -3558,7 +3599,7 @@ const AppCreator = (defaultprops) => { placeholder={"Query name (key)"} label={"Query Key"} helperText={ - + Click required to flip } @@ -3570,7 +3611,7 @@ const AppCreator = (defaultprops) => { style={{flex: 3}} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3590,7 +3631,7 @@ const AppCreator = (defaultprops) => { style={{flex: 2}} InputProps={{ style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -3663,7 +3704,7 @@ const AppCreator = (defaultprops) => { )} { }} key={currentAction} helperText={ - + Shows an example body to the user. ${} creates variables. } @@ -3688,7 +3729,8 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, }} /> @@ -3701,7 +3743,7 @@ const AppCreator = (defaultprops) => { Example success response { defaultValue={currentAction["example_response"]} onChange={(e) => setActionField("example_response", e.target.value)} helperText={ - + Helps with autocompletion and understanding of the endpoint } key={currentAction} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, }} /> @@ -3795,8 +3838,8 @@ const AppCreator = (defaultprops) => { fullWidth PaperProps={{ style: { - backgroundColor: surfaceColor, - color: "white", + backgroundColor: theme.palette.drawer.backgroundColor, + color: theme.palette.text.primary, minWidth: 700, maxWidth: 700, }, @@ -3813,15 +3856,15 @@ const AppCreator = (defaultprops) => { setFileUploadEnabled(false); }} > - + -
New action
+
New action
Learn more about actions @@ -3833,7 +3876,8 @@ const AppCreator = (defaultprops) => { flex: "1", marginTop: 5, marginRight: 15, - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }} fullWidth={true} placeholder="Name" @@ -3858,7 +3902,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 } } @@ -3872,7 +3916,8 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} /> @@ -3884,7 +3929,8 @@ const AppCreator = (defaultprops) => { flex: "1", marginTop: 5, marginRight: "15px", - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }} fullWidth={true} placeholder="Description" @@ -3896,7 +3942,8 @@ const AppCreator = (defaultprops) => { onChange={(e) => setActionField("description", e.target.value)} InputProps={{ style: { - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} /> @@ -3918,14 +3965,18 @@ const AppCreator = (defaultprops) => { }} value={currentActionMethod} style={{ - backgroundColor: inputColor, paddingLeft: "10px", - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, height: "50px", }} inputProps={{ name: "Method", id: "method-option", + style: { + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, + }, }} > @@ -3940,7 +3991,7 @@ const AppCreator = (defaultprops) => { > { flex: "1", marginRight: "15px", marginTop: "5px", - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }} fullWidth={true} placeholder="URL path" @@ -3977,7 +4029,7 @@ const AppCreator = (defaultprops) => { setUrlPath(e.target.value); }} helperText={ - + The path to use. Must start with /. Use {"{variablename}"} to have path variables @@ -3988,7 +4040,8 @@ const AppCreator = (defaultprops) => { input: classes.input, }, style: { - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} onBlur={(event) => { @@ -4271,7 +4324,7 @@ const AppCreator = (defaultprops) => { defaultValue={currentAction["file_field"]} onChange={(e) => setActionField("file_field", e.target.value)} helperText={ - + The File field to interact with } @@ -4280,7 +4333,7 @@ const AppCreator = (defaultprops) => { notchedOutline: classes.notchedOutline, }, style: { - color: "white", + color: theme.palette.text.primary, }, }} /> @@ -4295,7 +4348,8 @@ const AppCreator = (defaultprops) => { flex: "1", marginRight: "15px", marginTop: "5px", - backgroundColor: inputColor, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }} fullWidth={true} placeholder={ @@ -4309,13 +4363,14 @@ const AppCreator = (defaultprops) => { minRows="2" onChange={(e) => setActionField("headers", e.target.value)} helperText={ - + Headers that are part of the request. Default: EMPTY } InputProps={{ style: { - color: "white", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, }, }} /> @@ -4324,14 +4379,14 @@ const AppCreator = (defaultprops) => { {bodyInfo} {exampleResponse} -
+
@@ -3920,13 +3923,13 @@ const AppExplorer = (props) => { -

+

Apps

@@ -3972,9 +3975,7 @@ const AppExplorer = (props) => { ) : null} {appType === 0 || appType === 2 ? ( { const data = openapi; let exportFileDefaultName = name + ".json"; @@ -3995,7 +3996,7 @@ const AppExplorer = (props) => { if (queryID !== undefined && queryID !== null) { aa("init", { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }); const timestamp = new Date().getTime(); @@ -4021,12 +4022,12 @@ const AppExplorer = (props) => { }} > - + ) : null} - {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined && userdata.support === true ? + {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined ? // Iconbutton for authentication with just an icon. Link to /apps/authentication?app_id=app.id { }} > - + : null} @@ -4065,7 +4066,7 @@ const AppExplorer = (props) => { } - {isMobile ? null : ( + {isMobile || app?.reference_org === userdata?.active_org?.id ? null : ( )} {appType === 1 ? null : - creatorProfile.self === true || userdata.support === true ? + userdata?.support || userdata?.id === app?.owner || (userdata?.admin === "true" && userdata?.active_org?.id === app?.reference_org) || app?.contributors?.includes(userdata?.id) || creatorProfile?.self === true ? - {(appdata?.owner === userdata?.id) ? ( - ): null }
) : ( { style={{ borderRadius: 25, fontSize: 11, - color: "white", - backgroundColor: "rgba(255,255,255,0)", + color: theme.palette.text.primary, + backgroundColor: theme.palette.platformColor, border: "1px solid #ddf4e1", textTransform: "none", marginLeft: 4, @@ -4468,6 +4468,11 @@ const AppExplorer = (props) => { color: "white", textAlign: "center", }} + sx={{ + "&:hover": { + backgroundColor: theme.palette.hoverColor, + }, + }} > { color: "white", textAlign: "center", }} + sx={{ + "&:hover": { + backgroundColor: theme.palette.hoverColor, + }, + }} > { return parsedValues; }; -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Apps = (props) => { const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props; @@ -1305,7 +1305,7 @@ const Apps = (props) => { if (queryID !== undefined && queryID !== null) { aa("init", { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }); const timestamp = new Date().getTime(); @@ -2036,7 +2036,7 @@ const Apps = (props) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "db08e40265e2941b9a7d8f644b6e5240", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx index c78eabe6..cdfebe78 100644 --- a/frontend/src/views/Apps2.jsx +++ b/frontend/src/views/Apps2.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useContext, useCallback, memo, useMemo, useRef } from "react"; -import theme from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import { isMobile } from "react-device-detect"; + import { useLocation, useNavigate } from "react-router-dom"; import { TextField, Button, Typography, MenuItem, Select, Tabs, Tab, Zoom, @@ -45,19 +46,20 @@ import AppCreationModal from "../components/AppCreationModal.jsx"; const searchClient = algoliasearch( "JNSS5CFDZZ", - "db08e40265e2941b9a7d8f644b6e5240" + "c8f882473ff42d41158430be09ec2b4e" ); // AppCard Component const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, deactivatedIndexes, currTab, handleAppClick, leftSideBarOpenByClick, userdata, fetchApps, appsToShow, setAppsToShow, setUserApps, }) => { const navigate = useNavigate(); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "localhost:3000"; - const appUrl = isCloud ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`; - - var canEditApp = userdata?.support || - userdata?.id === data?.owner || - (userdata?.admin === "true" && userdata?.active_org?.id === data?.reference_org) || - data?.contributors?.includes(userdata?.id) + //const appUrl = isCloud ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`; + const appUrl = `/apps/${data.id}` + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); + + var canEditApp = userdata?.support || userdata?.id === data?.owner || + (userdata?.admin === "true" && userdata?.active_org?.id === data?.reference_org) || data?.contributors?.includes(userdata?.id) const paperStyle = { backgroundColor: mouseHoverIndex === index ? "rgba(26, 26, 26, 1)" : "#212121", @@ -71,7 +73,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)", marginBottom: 20, transition: "width 0.3s ease", - }; + } return ( @@ -104,7 +106,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, display: "flex", fontFamily: theme?.typography?.fontFamily, width: '100%', - backgroundColor: mouseHoverIndex === index ? "#2F2F2F" : "#212121" + backgroundColor: mouseHoverIndex === index ? theme.palette.hoverColor : theme.palette.platformColor, }} onClick={() => { handleAppClick(data); @@ -133,6 +135,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, margin: "12px 0", fontFamily: theme?.typography?.fontFamily, }}> +
+
- {data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())} + {data?.name?.replace(/_/g, ' ')?.replace(/\b\w/g, char => char?.toUpperCase())}
+
- {data.categories ? data.categories.join(", ") : "NA"} + {data?.categories ? data?.categories?.join(", ") : "NA"}
+
- {data.generated !== true && data.tags && data.tags.slice(0, 2).map((tag, tagIndex) => ( + {data?.generated !== true && data?.tags && data?.tags?.slice(0, 2).map((tag, tagIndex) => ( {tag} {tagIndex < data.tags.length - 1 ? ", " : ""} ))}
+ {/* Deactivate button */} {(currTab === 0 || currTab === 1) && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && (
{ canEditApp ? ( - + ) : ( - + ) } +
@@ -576,7 +571,7 @@ const Hits = ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", - color: "rgba(158, 158, 158, 1)", + color: theme.palette.text.secondary, marginTop: 5, }} > @@ -598,7 +593,7 @@ const Hits = ({
- {hoverEffect === index && isCloud ? ( + {hoverEffect === index ? (
{data.tags && ( )}
+
{hoverEffect === index && (
- {allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? ( + {allActivatedAppIds && allActivatedAppIds?.includes(data.objectID) ? ( + ) : ( + @@ -2228,7 +2222,10 @@ const Apps2 = (props) => {
{ - currTab === 0 && ( + currTab !== 0 && currTab !== 3 ? + null + : + (
{isLoading ? ( @@ -2260,7 +2257,7 @@ const Apps2 = (props) => { handleAppClick={handleAppClick} leftSideBarOpenByClick={leftSideBarOpenByClick} userdata={userdata} - fetchApps={fetchApps} + fetchApps={getApps} setUserApps={setUserApps} appsToShow={appsToShow} @@ -2313,7 +2310,7 @@ const Apps2 = (props) => { {appsToShow.map((data, index) => ( { //axis={} } + +// 0x0elliot: not cleaning this up. this seems clearly experimental. not worth my time. + // This is the start of a dashboard that can be used. // What data do we fill in here? Idk const Dashboard = (props) => { + const { globalUrl, isLoggedIn } = props; //const alert = useAlert(); const [bigChartData, setBgChartData] = useState("data1"); @@ -397,7 +398,7 @@ const Dashboard = (props) => { if (foundQuery !== null && foundQuery !== undefined) { setSelectedUsecaseCategory(foundQuery) - const newitem = removeParam("selected", cursearch); + const newitem = removeParam("selected", cursearch); navigate(curpath + newitem) } diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 836ada35..2456f63d 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -1,11 +1,11 @@ -import React, { useEffect, useLayoutEffect, useRef, useState, useContext, memo } from "react" +import React, { useEffect, useLayoutEffect, useRef, useState, useContext, memo, } from "react" import { toast } from 'react-toastify'; import Markdown from 'react-markdown' -import theme from '../theme.jsx'; +import {getTheme} from '../theme.jsx'; import ReactJson from "react-json-view-ssr"; import { isMobile } from "react-device-detect"; import { BrowserView, MobileView } from "react-device-detect"; -import { useParams, useNavigate, Link } from "react-router-dom"; +import { useParams, useNavigate, Link, useLocation } from "react-router-dom"; import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import remarkGfm from 'remark-gfm' import { Context } from "../context/ContextApi.jsx"; @@ -23,16 +23,27 @@ import { List, Collapse, ListItemButton, - ListItemText + ListItemText, + Dialog, + DialogTitle, + Box, + DialogContent, + InputAdornment, + CircularProgress } from "@mui/material"; import { Link as LinkIcon, Edit as EditIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ExpandMore as ExpandMoreIcon, - FileCopy as FileCopyIcon + FileCopy as FileCopyIcon, + ChevronLeft, + ChevronRight, + Search as SearchIcon, + Close as CloseIcon } from "@mui/icons-material"; import { fontGrid } from "@mui/material/styles/cssUtils.js"; +import SearchBox from "../components/SearchData.jsx"; const Body = { //maxWidth: 1000, @@ -51,6 +62,7 @@ const dividerColor = "rgb(225, 228, 232)"; const hrefStyle = { color: "rgba(255, 255, 255, 0.8)", textDecoration: "none", + marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, }; @@ -152,11 +164,37 @@ export const OuterLink = (props) => { } + export const Img = (props) => { // Find parent container and check width - + const isArticlePage = window.location.pathname.includes("/articles/") + const isFormPage = window.location.pathname.includes("/forms/") var height = "auto" - var width = 750 + var width = isArticlePage ? 1000 : isFormPage ? 400: 750 + + const { themeMode } = useContext(Context) + const theme = getTheme(themeMode) + + const docsImageStyle = { + border: isFormPage ? null : "1px solid rgba(255,255,255,0.3)", + borderRadius: theme.palette?.borderRadius, + width: width, + maxWidth: width, + margin: "auto", + marginTop: 10, + marginBottom: 10 + } + + const articleImageStyle = { + borderRadius: theme.palette?.borderRadius, + minWidth: 350, + maxWidth: "100%", + textAlign: "center", + margin: "auto", + marginTop: 20, + marginBottom: 20, + } + if (props.height !== undefined && props.height !== null) { height = props.height } @@ -166,11 +204,13 @@ export const Img = (props) => { } return( - {props.alt} +
+ {props.alt} +
) } @@ -179,6 +219,8 @@ export const CodeHandler = (props) => { const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : "" const validate = validateJson(propvalue) + const {themeMode } = useContext(Context) + const theme = getTheme(themeMode) var newprop = propvalue if (validate.valid === false) { @@ -251,8 +293,13 @@ export const CodeHandler = (props) => { } const Docs = (defaultprops) => { - const { globalUrl, selectedDoc, serverside, serverMobile, isLoggedIn, isLoaded } = defaultprops; + const { globalUrl, selectedDoc, serverside, serverMobile, isLoggedIn, isLoaded, userdata } = defaultprops; + + const { searchBarModalOpen, setSearchBarModalOpen, isDocSearchModalOpen, setIsDocSearchModalOpen, leftSideBarOpenByClick } = useContext(Context); + let navigate = useNavigate(); + const location = useLocation(); + const pathname = location.pathname // Quickfix for react router 5 -> 6 const params = useParams(); //var props = JSON.parse(JSON.stringify(defaultprops)) @@ -261,6 +308,8 @@ const Docs = (defaultprops) => { props.match.params = params //console.log("PARAMS: ", params) + const { themeMode } = useContext(Context) + const theme = getTheme(themeMode) const [mobile, setMobile] = useState(serverMobile === true || isMobile === true ? true : false); const [data, setData] = useState(""); @@ -283,16 +332,75 @@ const Docs = (defaultprops) => { serverside === true ? "" : window.location.href ); const [hashRendered, setHashRendered] = React.useState(false) - + const [sidebarOpen, setSidebarOpen] = useState(false); + const [activeSubItem, setActiveSubItem] = useState(false); const headingElementsRef = useRef({}) + const [hasRedirected, setHasRedirected] = useState(false) + var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false; + const searchFieldRef = useRef(null); + + const handleDocRedirectForPartners = () => { + if (hasRedirected) return; + + if ( + userdata && + userdata?.org_status?.includes("integration_partner") && + userdata?.active_org?.branding?.documentation_link?.length > 0 + ) { + const docLink = userdata?.active_org?.branding?.documentation_link; + if (docLink && docLink !== "") { + setHasRedirected(true); + if (docLink.startsWith('http')) { + window.location.replace(docLink); + } else { + navigate(docLink); + } + } + } + }; + useEffect(() => { - //if (params["key"] === undefined) { - // navigate("/docs/about") - // return - //} - }, []) + if (isLoggedIn && isLoaded) { + handleDocRedirectForPartners() + } + }, [isLoggedIn, isLoaded]); + + useEffect(() => { + fetchDocList(); + setSidebarOpen(false); + + if (props.match.params === undefined || props.match.params === null) { + return + } + + const propkey = props.match.params.key + if (propkey === undefined || propkey === null) { + 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]); + + useEffect(() => { + return () => { + setIsDocSearchModalOpen(false); + setSearchBarModalOpen(false); + }; + }, []); + const handleClick = (event) => { setAnchorEl(event.currentTarget); } @@ -422,6 +530,77 @@ const Docs = (defaultprops) => { return headings; }; + const modalView = ( + { + setSearchBarModalOpen(false); + if (searchFieldRef.current) { + searchFieldRef.current.blur(); + } + }} + PaperProps={{ + sx: { + color: theme.palette.DialogStyle.color, + minWidth: "750px", + minHeight: "180px", + maxHeight: "85vh", + borderRadius: theme.palette.DialogStyle.borderRadius, + border: "1px solid var(--Container-Stroke, #494949)", + background: theme.palette.DialogStyle.backgroundColor, + boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", + position: "fixed", + top: "70px", + left: "50%", + transform: "translateX(-50%)", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + sx={{ + zIndex: 50005, + '& .MuiBackdrop-root': { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, + }} + > + + + Search for Documentation + + setSearchBarModalOpen(false)} + sx={{ + color: 'white', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }} + > + + + + + + + + + + + ); + // extract TOC from actual markdown const tocvalue = (markdown) => { @@ -460,8 +639,13 @@ const Docs = (defaultprops) => { } const SidebarPaperStyle = { - backgroundColor: "rgb(26,26,26)", + backgroundColor: isArticlePage ? "transparent" : theme.palette.backgroundColor, + border: isArticlePage ? "none" : undefined, + borderRadius: isArticlePage ? "none" : undefined, + boxShadow: isArticlePage ? "none" : undefined, backgroundImage: "none", + width: isArticlePage ? "100%" : undefined, + height: isArticlePage ? "100%" : undefined, overflowX: "hidden", position: "relative", paddingLeft: 15, @@ -524,10 +708,10 @@ const Docs = (defaultprops) => { extraInfo = (
@@ -540,7 +724,7 @@ const Docs = (defaultprops) => { href={selectedMeta.link} style={{ textDecoration: "none", color: "#f85a3e" }} > - @@ -620,7 +804,7 @@ const Docs = (defaultprops) => { style={{ width: "90%", marginTop: 60, - marginBottom: 20, + marginBottom: isArticlePage ? 40 : 20, backgroundColor: theme.palette.inputColor, }} /> @@ -630,16 +814,29 @@ const Docs = (defaultprops) => { marginBlock: "0.85em", alignItems: "center" }}> {element} - { + e.preventDefault(); // Prevent default navigation + document.getElementById(id)?.scrollIntoView({ + behavior: 'smooth' + }); + + const url = `${window.location.origin}${window.location.pathname}#${id}`; + navigator.clipboard.writeText(url); + toast("Link copied to clipboard", { + position: "bottom-center", + autoClose: 2000, + }); + }} + style={{ textDecoration: "none", color: "white", paddingLeft: "0.3em", rotate: "-30deg", paddingTop: "0.9em", display: props.level === 1 ? "none" : "block", }}> - +
- - {extraInfo} + {isArticlePage ? (userdata?.support ? extraInfo : "") : extraInfo} ) } @@ -647,21 +844,56 @@ const Docs = (defaultprops) => { const SideBar = { width: "17%", + borderRight: !isArticlePage ? "1px solid rgba(255,255,255,0.3)" : undefined, + minWidth: isArticlePage ? "250px" : "17%", + maxWidth: isArticlePage ? "250px" : "17%", + marginLeft: isArticlePage ? sidebarOpen ? 40 : 50 : undefined, position: "sticky", - top: 50, - paddingTop: "0.25em", - minHeight: "95vh", - maxHeight: "95vh", + top: isArticlePage ? 120 : 50, + paddingTop: isArticlePage ? "0.65em" : "0.25em", + paddingRight: isArticlePage ? "1em" : undefined, + minHeight: isArticlePage ? "80vh" : "95vh", + maxHeight: isArticlePage ? "80vh" : "95vh", overflowX: "hidden", overflowY: "auto", zIndex: 1000, - //borderRight: "1px solid rgba(255,255,255,0.3)", + backgroundColor: isArticlePage ? "#212121" : undefined, + borderRadius: isArticlePage ? theme.palette?.borderRadius : undefined, + transition: isArticlePage ? "width 0.3s ease, min-width 0.3s ease" : undefined, + }; + + const sideBarToggleButton = { + position: "absolute", + bottom: sidebarOpen ? 20 : "50%", + right: sidebarOpen ? 30 : 21, + zIndex: 1000, + backgroundColor: "#212121", + border: "1px solid rgba(255,255,255,0.3)", + borderRadius: "50%", + width: 40, + height: 40, + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + transition: "transform 0.3s ease", + "&:hover": { + backgroundColor: "#2c2c2c", + } + }; + + const collapsedSideBar = { + ...SideBar, + width: "40px", + minWidth: "40px", + maxWidth: "40px", + overflow: "hidden", }; const IndexBar = { alignSelf: "flex-start", position: "sticky", - top: 80, + top: 120, overflowY: "auto", minHeight: "93vh", maxHeight: "93vh", @@ -670,8 +902,15 @@ const Docs = (defaultprops) => { overflow: "hidden", } - const fetchDocList = () => { - fetch(`${globalUrl}/api/v1/docs`, { + const fetchDocList = (resetCache = false) => { + var url = `${globalUrl}/api/v1/docs` + if (location.pathname.includes("/legal")) { + url = `${globalUrl}/api/v1/docs?folder=legal&resetCache=${resetCache}` + } else if (location.pathname.includes("/articles")) { + url = `${globalUrl}/api/v1/docs?folder=articles&resetCache=${resetCache}` + } + + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -691,8 +930,19 @@ const Docs = (defaultprops) => { .catch((error) => { }); }; - const fetchDocs = (docId) => { - fetch(`${globalUrl}/api/v1/docs/${docId}`, { + const fetchDoc = (docId) => { + if (docId === undefined) { + return + } + + var url = `${globalUrl}/api/v1/docs/${docId}` + if (location.pathname.includes("/legal")) { + url = `${globalUrl}/api/v1/docs/${docId}?folder=legal` + } else if (location.pathname.includes("/articles")) { + url = `${globalUrl}/api/v1/docs/${docId}?folder=articles` + } + + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -706,19 +956,33 @@ const Docs = (defaultprops) => { } if (responseJson.success && responseJson.reason !== undefined) { + + if(isArticlePage && window.location.pathname.includes("/articles/")) { + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) { + setData("# Error\nThis page doesn't exist."); + toast("This page doesn't exist. Redirecting to the latest article...", { + position: "bottom-center", + autoClose: 2000, + }); + setTimeout(() => { + navigate(`/articles/2.0_release`) + }, 2000) + return + } + } // Find tags and translate them into ![]() format const imgRegex = / { .catch((error) => { }); }; + const handleResetCache = () => { + fetchDocList(true); + toast("Cache has been reset"); + } + if (firstrequest) { setFirstrequest(false); if (!serverside) { @@ -769,11 +1038,29 @@ const Docs = (defaultprops) => { // return null //} // - if (props.match.params.key === undefined) { - + if (props.match.params.key === undefined && isArticlePage) { + fetch(`${globalUrl}/api/v1/articles`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success && responseJson.list && responseJson.list.length > 0) { + // Navigate to the latest article + if (responseJson?.list[0]?.name) { + navigate(`/articles/${responseJson?.list[0].name}`); + } + } + }) + .catch((error) => { + console.error("Error fetching articles:", error); + }); } else { console.log("DOCID: ", props.match.params.key) - fetchDocs(props.match.params.key) + fetchDoc(props.match.params.key) } } } @@ -782,18 +1069,17 @@ const Docs = (defaultprops) => { // Handles search-based changes that origin from outside this file if (serverside !== true && window.location.href !== baseUrl) { setBaseUrl(window.location.href); - fetchDocs(props.match.params.key); + fetchDoc(props.match.params.key); } const markdownStyle = { - color: "rgba(255, 255, 255, 0.90)", + color: theme.palette.textColor, overflow: "hidden", paddingBottom: 100, margin: "auto", maxWidth: "100%", minWidth: "100%", - overflow: "hidden", - fontSize: isMobile ? "1.3rem" : "1rem", + fontSize: isMobile ? "1.3rem" : "1.1rem", }; const alertNote = { @@ -872,6 +1158,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 =
{ Documentation -
- /> - link="https://discord.gg/B2CBzUm" /> -
+ {showPartnerLogo === true ? null : +
+ /> + link="https://discord.gg/B2CBzUm" /> +
+ }
Tutorial @@ -940,20 +1229,94 @@ const Docs = (defaultprops) => { } + const activeHrefStyleToc2 = { + ...hrefStyleToc2, + color: "#f86a3e", + fontFamily: theme.typography.fontFamily, + }; + + const activeListItemStyle = { + backgroundColor: "rgba(248, 106, 62, 0.08)", // Slight orange tint + marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, + borderLeft: "3px solid #f86a3e", + paddingLeft: "13px", // Compensate for the border + }; + + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + // PostDataBrowser Section const postDataBrowser = list === undefined || list === null ? null : (
-
+
- + {modalView} + { + !isArticlePage && ( + { + setIsDocSearchModalOpen(true) + setSearchBarModalOpen(true) + }} + inputRef={searchFieldRef} + InputProps={{ + endAdornment: ( + + + + ), + style: { + backgroundColor: theme.palette.platformColor, + borderRadius: 4, + fontSize: 18, + color: theme.palette.textColor, + }, + }} + style={{ + marginTop: 10, + marginLeft: 10, + borderRadius: 4, + width: !isLoggedIn ? "14%" : !leftSideBarOpenByClick ? "12%" : "11%", + maxWidth: "285px", + position: "fixed", + zIndex: 1100, + }} + inputProps={{ + readOnly: true, + }} + /> + ) + } + {list.map((data, index) => { const item = data.name; if (item === undefined) { return null; } - const path = "/docs/" + item; + + var path = "/docs/" + item; + if (location.pathname.includes("/legal")) { + path = "/legal/" + item + } else if (location.pathname.includes("/articles")) { + path = "/articles/" + item + } + const newname = item.charAt(0).toUpperCase() + item.substring(1).split("_").join(" ").split("-").join(" "); @@ -965,23 +1328,63 @@ const Docs = (defaultprops) => { - {newname} - + style={{ + color: itemMatching ? "#f86a3e" : theme.palette.textColor, + flex: 1, + }} + primary={ + + {newname} + + } + /> + ); })} + { + isArticlePage && ( + + {sidebarOpen ? : } + + ) + }
-
+
{props.match.params.key === undefined ? mainpageInfo : @@ -1003,12 +1406,26 @@ const Docs = (defaultprops) => { }
+ {userdata?.support && isArticlePage && ( + + )} {tocLines.length > 0 ? ( -

Table of Content

+

Table Of Content

) : null} - +
@@ -1083,7 +1508,7 @@ const Docs = (defaultprops) => { color="primary" onClick={handleClick} > -
More docs
+
More {isArticlePage ? "articles" : "docs"}
{ return null; } - const path = "/docs/" + item; + const path = isArticlePage ? "/articles/" + item : "/docs/" + item; const newname = item.charAt(0).toUpperCase() + item.substring(1).split("_").join(" ").split("-").join(" "); @@ -1108,7 +1533,8 @@ const Docs = (defaultprops) => { key={index} style={{ color: "white" }} onClick={() => { - window.location.pathname = path; + navigate(path) + handleClose() }} > {newname} @@ -1140,8 +1566,22 @@ const Docs = (defaultprops) => { { + console.log(`Scrolling to: ${data.id}`); + setTimeout(() => { + const element = document.getElementById(data?.id); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } else { + console.error(`Element with id ${data.id} not found.`); + } + }, 100); // Delay to ensure the content is loaded + handleCloseToc(); + }} > - {data.title} + {data.title} ) })} @@ -1178,14 +1618,14 @@ const Docs = (defaultprops) => { color="primary" onClick={handleClick} > -
More docs
+
More {isArticlePage ? "articles" : "docs"}
); // Padding and zIndex etc set because of footer in cloud. const loadedCheck = ( - + ); @@ -1197,25 +1637,41 @@ return
{loadedCheck}
; export default Docs; const DocsContent = memo(({postDataBrowser, postDataMobile}) => { + const { themeMode } = useContext(Context); + const theme = getTheme(themeMode); return( -
+
{postDataBrowser} {postDataMobile}
)}) -const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{ +const DocsWrapper = memo(({isLoggedIn, isLoaded, children, userdata })=>{ const { leftSideBarOpenByClick, windowWidth } = useContext(Context); + useEffect(() => { + if (isLoaded && isLoggedIn && userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.branding.documentation_link?.length > 0) { + window.location.href = userdata?.active_org?.branding.documentation_link; + } + }, [isLoaded, isLoggedIn, userdata]); + + if (isLoaded && isLoggedIn && userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.branding.documentation_link?.length > 0) { + return ; + } + + + return (
{children} diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 719a4717..b717b9f6 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -1,546 +1,1049 @@ /* eslint-disable react/no-multi-comp */ -import React, { useState, useEffect } from "react"; -import { makeStyles } from "@mui/styles"; +import React, { useState, useEffect, } from 'react'; +import { makeStyles } from '@mui/styles'; +import { useNavigate, Link, useParams } from "react-router-dom"; +import { isMobile } from "react-device-detect"; +import { toast } from 'react-toastify'; import { useInterval } from "react-powerhooks"; -import theme from '../theme.jsx'; +import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; +import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; +import { + ConnectingAirportsOutlined, + ConstructionOutlined, + DoneRounded, + Done as DoneIcon +} from '@mui/icons-material'; import { - CircularProgress, - TextField, - Button, - Paper, - Typography, -} from "@mui/material"; - -import { useNavigate } from "react-router-dom"; + Checkbox, + CircularProgress, + TextField, + Button, + Paper, + Typography, + Tooltip, + TableHead, + TableRow, + TableContainer, + TableCell, + TableBody, + Table, +} from '@mui/material'; const hrefStyle = { - color: "white", - textDecoration: "none", -}; + color: "#FF8444", + fontSize: "14px", + textDecoration: "none", + display: "flex", +} -const bodyDivStyle = { - margin: "auto", - marginTop: 150, - width: "500px", -}; +const googleLoginIcon = { + boxSizing: "border-box", + display: "flex", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + padding: "16px", + gap: "8px", + // position: "sticky", + width: 171, + height: "51px", + left: "352px", + top: "725px", + background: "#1A1A1A", + border: "1px solid #494949", + borderRadius: "8px", + +} + +const githubLoginIcon = { + boxSizing: "border-box", + display: "flex", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + padding: "16px", + gap: "8px", + position: "sticky", + width: "173px", + height: "51px", + left: "539px", + top: "725px", + backgroundColor: "#1A1A1A", + border: "1px solid #494949", + borderRadius: "8px", +} + +const surfaceColor = "#27292D" +const inputColor = "#383B40" const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important", - }, + notchedOutline: { + borderColor: "#f85a3e !important" + }, + marketplaceIcon: { + width: 28, + height: 28, + marginRight: 12 + }, + divider: { + display: "flex", + alignItems: "center", + margin: "0 20px", + '&::before, &::after': { + content: '""', + flex: 1, + borderBottom: "1px solid #494949" + }, + '& span': { + margin: "0 10px", + color: "#9E9E9E" + } + }, + freePlanCard: { + padding: "40px", + background: "#212121", + borderRadius: "12px", + width: "100%", + maxWidth: "500px" + }, + freePlanTitle: { + fontSize: "28px", + fontWeight: 600, + color: "white", + marginTop: 0, + marginBottom: "32px" + }, + featureItem: { + display: "flex", + alignItems: "center", + marginBottom: "20px", + color: "white", + fontSize: "16px", + fontWeight: 500 + }, + checkIcon: { + color: "#4CAF50", + marginRight: "16px", + width: "24px", + height: "24px" + } }); -const LoginDialog = (props) => { - const { - globalUrl, - isLoaded, - isLoggedIn, - setIsLoggedIn, - setCookie, - register, - checkLogin, - } = props; +const FreePlanCard = ({ classes }) => { + const features = [ + "Access to All Apps", + "10,000 App Runs", + "Monthly Runs Refresh", + "Unlimited Users & Workflows", + "Multi-Tenancy & -Region", + "Support & Discord Access" + ]; + + return ( +
+

+ The free plan includes: +

+ + {features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ ); +}; + +const MarketplaceCard = ({ classes }) => { + const marketplaceOptions = [ + { + name: "Open Source Install", + logo: "https://static.cdnlogo.com/logos/g/69/github-icon.svg", + tooltipText: "Click to install!", + valid: true, + + link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md" + }, + { + name: "Amazon Web Services", + logo: "https://cdn.cdnlogo.com/logos/a/19/aws.svg", + tooltipText: "Coming soon to AWS Marketplace!", + valid: false, + }, + { + name: "Microsoft Azure", + logo: "https://cdn.cdnlogo.com/logos/a/12/azure.svg", + tooltipText: "Coming soon to Azure Marketplace!", + valid: false, + }, + { + name: "Google Cloud Platform", + logo: "https://cdn.cdnlogo.com/logos/g/75/google-cloud.svg", + tooltipText: "Coming soon to Google Cloud Marketplace!", + valid: false, + } + ]; + + return ( +
+

+ Self Host +

+ + {marketplaceOptions.map((option, index) => { + return ( + + + + ) + })} + + + +
+ ); +}; + + +const LoginPage = props => { + const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, inregister, serverside, checkLogin, } = props; let navigate = useNavigate(); - const classes = useStyles(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loginLoading, setLoginLoading] = useState(false); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [firstRequest, setFirstRequest] = useState(true); - const [loginLoading, setLoginLoading] = useState(false); - const [loginViewLoading, setLoginViewLoading] = useState(false); - const [ssoUrl, setSSOUrl] = useState(""); + const [MFAField, setMFAField] = useState(false); + const [MFAValue, setMFAValue] = useState(""); + const [register, setRegister] = useState(inregister); + const [checkboxClicked, setCheckboxClicked] = useState(false); + const [loginWithSSO, setLoginWithSSO] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [ssoUrl, setSSOUrl] = useState(""); - const [MFAField, setMFAField] = useState(false); - const [MFAValue, setMFAValue] = useState(""); - - - // Used to swap from login to register. True = login, false = register + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io" || window.location.host === "sandbox.shuffler.io"; + const parsedsearch = serverside === true ? "" : window.location.search useEffect(() => { - checkAdmin() - }, [loginViewLoading]) + if (!isCloud) { + checkAdmin() + } + }, []) - // Error messages etc - const [loginInfo, setLoginInfo] = useState(""); - - const handleValidateForm = () => { - return username.length > 1 && password.length > 1; - }; - - if (isLoggedIn === true) { - //window.location.pathname = "/workflows"; - navigate("/workflows") - } - - const checkAdmin = () => { - const url = globalUrl + "/api/v1/checkusers"; - fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + checkAdmin() }, }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { - setSSOUrl(responseJson.sso_url); - } + useEffect(() => { + if (serverside !== true) { + const tmpMessage = new URLSearchParams(window.location.search).get("message") + if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { + setMessage(tmpMessage) + toast(tmpMessage) + } + } + }, []) - if (loginViewLoading) { - setLoginViewLoading(false); - checkLogin(); - stop(); - - if ( - responseJson.reason !== undefined && - responseJson.reason !== null - ) { - setLoginInfo(responseJson.reason); - } - } - - if (responseJson.reason === "stay") { - navigate("/adminsetup") - } - } - }) - ) - .catch((error) => { - if (!loginViewLoading) { - setLoginViewLoading(true); - start(); - } - }); - }; - - const { start, stop } = useInterval({ - duration: 3000, - startImmediate: false, - callback: () => { - checkAdmin(); - }, - }); - - if (firstRequest) { - setFirstRequest(false); - checkAdmin(); + if (document !== undefined) { + if (register) { + if (isCloud) { + document.title = "Login to Shuffle SaaS" + } else { + document.title = "Login to Shuffle" + } + } else { + if (isCloud) { + document.title = "Register to Shuffle SaaS" + } else { + document.title = "Register to Shuffle" + } + } } + + useEffect(() => { - const onSubmit = (e) => { - setLoginLoading(true); - e.preventDefault(); - setLoginInfo(""); - // FIXME - add some check here ROFL - - // Just use this one? - var data = { username: username, password: password }; - if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { - data["mfa_code"] = MFAValue; + if (loginWithSSO && window?.location?.pathname === "/register") { + setLoginWithSSO(false) } - var baseurl = globalUrl; - if (register) { - var url = baseurl + "/api/v1/login"; + },[window?.location?.pathname]) + + // Just a way to force location loading properly + // Register & login should be split :3 + if (window !== undefined) { + const path = window.location.pathname; + if (path.includes("/login") && register === false) { + console.log("Should register instead of login!") + setRegister(!register) + } else if (path.includes("/register") && register === true) { + if (!isCloud) { + setRegister(true) + navigate("/login") + } + + console.log("Should login instead of register!") + setRegister(!register) + } else { + //console.log("Path: " + path, "Register: " + register) + } + } + + const bodyDivStyle = { + marginTop: 100, + width: isMobile ? "100%" : "100%", + maxWidth: "1200px", // Increased max-width to accommodate larger cards + background: "#1A1A1A", + margin: "auto", + display: isMobile ? "block" : "flex", + padding: "40px", + gap: "40px", + overflow: "hidden", + alignItems: "center" + }; + + const boxStyle = { + color: "white", + padding: "40px", + flex: 1, + maxWidth: isMobile ? "100%" : 410, + minWidth: isCloud ? 410 : 475, + background: "#212121", + borderRadius: "12px", + display: "flex", + // flexDirection: "column", + }; + + + + const paperStyleReg = { + width: 300, + height: 350, + background: "#212121", + borderRadius: "8px", + marginLeft: isMobile ? 80 : register ? "455px" : "485px", + marginTop: isMobile ? 10 : 110, + position: isMobile ? "" : "absolute", + + } + const paperStyleLog = { + position: "absolute", + width: isMobile ? "400px" : "532px", + padding: "0px 30px 0px", + // marginTop: "135px", + // background: "#212121", + borderRadius: "8px", + } + const createData = (icon, title) => { + return { + icon, + title, + } + } + + // Used to swap from login to register. True = login, false = register + const activeIcon = + const rows = [ + createData(activeIcon, "Access to All Apps"), + createData(activeIcon, "10,000 App Runs"), + createData(activeIcon, "Monthly Runs Refresh"), + createData(activeIcon, "Unlimited Users & Workflows"), + createData(activeIcon, "Multi-Tenancy & -Region"), + createData(activeIcon, "Support & Discord Access"), + ]; + const classes = useStyles(); + // Error messages etc + const [loginInfo, setLoginInfo] = useState(""); + + const handleValidateForm = (username, password) => { + if (loginWithSSO) { + return username.length > 1 + } + + if (!isCloud) { + return (username.length > 0 && password.length > 0); + } + + return (username.length > 1 && password.length > 8); + } + + if (isLoggedIn === true && serverside !== true) { + setTimeout(() => { + const tmpView = new URLSearchParams(window.location.search).get("view"); + if (tmpView !== undefined && tmpView !== null) { + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; + + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) { + const redirectId = localStorage.getItem("redirectId") + localStorage.removeItem("redirectId") + pathOnly = pathOnly + "/" + redirectId + } + window.location.replace(pathOnly); + } + return; + } + + window.location.pathname = "/workflows" + }, 2000); + } + + const checkAdmin = () => { + const url = globalUrl + "/api/v1/checkusers"; fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => - response.json().then((responseJson) => { - setLoginLoading(false); - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - if (responseJson["reason"] === "MFA_REDIRECT") { - setLoginInfo( - "MFA required. Please enter the 6-digit code from your authenticator" - ); - setMFAField(true); - return; - } else if (responseJson["reason"] === "MFA_SETUP") { - window.location.href = `/login/${responseJson.url}/mfa-setup`; - return; - } - - setLoginInfo("Successful login, rerouting"); - for (var key in responseJson["cookies"]) { - setCookie( - responseJson["cookies"][key].key, - responseJson["cookies"][key].value, - { path: "/" } - ); - } - - if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { - console.log("RUN Welcome!!") - setTimeout(() => { - navigate("/welcome?tab=2") - },200) - // window.location.pathname = "" - return - } - - const tmpView = new URLSearchParams(window.location.search).get("view") - if (tmpView !== undefined && tmpView !== null) { - //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` - const newUrl = `/${tmpView}` - window.location.pathname = newUrl - } else { - window.location.pathname = "/workflows" - } - - setIsLoggedIn(true); - } - }) - ) - .catch((error) => { - setLoginLoading(false); - setLoginInfo("Error logging in: " + error); - }); - } else { - url = baseurl + "/api/v1/users/register"; - fetch(url, { - method: "POST", - body: JSON.stringify(data), + method: "GET", headers: { "Content-Type": "application/json", }, }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - setLoginInfo("Successful register!"); - } - }) - ) - .catch((error) => { - setLoginInfo("Error in from backend: ", error); - }); - } - }; + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); - const onChangeUser = (e) => { - setUsername(e.target.value); - }; - - const onChangePass = (e) => { - setPassword(e.target.value); - }; - - //const onClickRegister = () => { - // if (props.location.pathname === "/login") { - // window.location.pathname = "/register" - // } else { - // window.location.pathname = "/login" - // } - - // setLoginCheck(!register) - //} - - //var loginChange = register ? (

Want to register? Click here.

) : (

Go back to login? Click here.

); - var formtitle = register ?
Login
:
Register
; - const imgsize = 100; - const basedata = ( -
- -
- -
- - {loginViewLoading ? ( -
- - Waiting for the Shuffle database to become available. This may - take up to a minute. - - - {loginInfo === undefined || - loginInfo === null || - loginInfo.length === 0 ? null : ( -
Database Response: {loginInfo}
- )} - - - - - - Are you sure Shuffle is{" "} - - installed correctly - - ? - - - - 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 2Gb of RAM available:{" "} -
-
- sudo chown -R 1000:1000 shuffle-database -
- - 2. Disable memory swap on the host: -
-
- sudo swapoff -a -
- - 3. Restart the database: -
-
- sudo docker restart shuffle-opensearch -
-
- - Need help?{" "} - - Join the Discord! - - -
- ) : ( - -

{formtitle}

- Username -
- -
- Password -
- -
- {MFAField === true ? ( -
- 2-factor code - { - setMFAValue(event.target.value); - }} - /> -
- ) : null} -
- -
-
{loginInfo}
- {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( -
- Or -
- -
-
- ) : null} - - )} -
-
- ); - - const loadedCheck = isLoaded ?
{basedata}
:
; - - useEffect(() => { - setTimeout(() => { - if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) { - //id="sso_button" - const ssoBtn = document.getElementById("sso_button"); - if (ssoBtn !== undefined && ssoBtn !== null) { - //console.log("SSO BTN: ", ssoBtn) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - var tmpView = new URLSearchParams(cursearch).get("autologin"); - if (tmpView !== undefined && tmpView !== null) { - if (tmpView === "true") { - console.log("Tmp: ", tmpView) - ssoBtn.click() - } - } - } + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + navigate("/loginsetup") } - }, 200); - }, [ssoUrl]) - return
{loadedCheck}
; -}; + } else { + if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { + setSSOUrl(responseJson.sso_url); + } -export default LoginDialog; + // Stay = 0 users + // Redirect = >1 user + if (responseJson.reason === "stay") { + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + } + } + }) + ) + .catch((error) => { + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + }) + } + + const onSubmit = (e) => { + //toast("Testing from login page") + + setMessage("") + setLoginLoading(true) + e.preventDefault() + + // Just use this one? + var data = { "username": username, "password": password } + if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { + data["mfa_code"] = MFAValue + } + + localStorage.setItem("globalUrl", "") + + var baseurl = globalUrl + if (register) { + var url = baseurl + '/api/v1/login'; + + if (loginWithSSO === true) { + url = baseurl + '/api/v1/login/sso' + setLoginInfo("Logging in with SSO. Please wait while we find a relevant org...") + } + + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for login:O!"); + } + + return response.json(); + }) + .then((responseJson) => { + + setLoginLoading(false) + + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]) + } else { + if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") { + toast.info("Set region to " + responseJson.region_url) + localStorage.setItem("globalUrl", responseJson.region_url) + } + + if (responseJson["reason"] === "MFA_REDIRECT") { + setLoginInfo("Enter the 6-digit MFA code.") + setMFAField(true) + return + + } + else if (responseJson["reason"] === "MFA_SETUP") { + window.location.href = `/login/${responseJson.url}/mfa-setup`; + return; + } + else if (responseJson["reason"] === "SSO_REDIRECT") { + //navigate(responseJson["url"]) + window.location.href = responseJson["url"] + return + } + else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) { + setLoginInfo(responseJson["reason"]) + return + } + + + setLoginInfo("Successful login! Redirecting you in 3 seconds...") + for (var key in responseJson["cookies"]) { + setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }) + } + + setTimeout(() => { + const tmpView = new URLSearchParams(window.location.search).get("view"); + if (tmpView !== undefined && tmpView !== null) { + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; + + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) { + const redirectId = localStorage.getItem("redirectId") + localStorage.removeItem("redirectId") + pathOnly = pathOnly + "/" + redirectId + } + window.location.replace(pathOnly); + } + return; + } + + if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) { + // Find welcome in responseJson.tutorials under key name + const welcome = responseJson.tutorials.find(function (element) { + return element.name === "welcome"; + }) + console.log("Welcome: ", welcome) + if (welcome === undefined || welcome === null) { + console.log("RUN login Welcome!!") + // window.location.pathname = "/welcome?tab=2" + // window.location = "/welcome?tab=2" + window.location.href = "/welcome?tab=2" + return + } + } + + window.location.pathname = "/workflows" + }, 2000); + } + }) + .catch(error => { + setLoginInfo("Error from login API: " + error) + setLoginLoading(false) + }); + } else { + url = baseurl + '/api/v1/register'; + fetch(url, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]) + } else { + if (responseJson["reason"] === "shuffle_account") { + window.location.href = "/login?message=Please+login+with+your+Shuffle+account" + return + } + + //setLoginInfo("Successful register!") + //var newpath = "/login?message=Successfully signed up. You can now sign in." + //const tmpMessage = new URLSearchParams(window.location.search).get("message") + + for (var key in responseJson["cookies"]) { + setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }) + } + + setLoginLoading(false) + setLoginInfo("Successful registration! Redirecting in 3 seconds...") + + + setTimeout(() => { + console.log("LOGIN DATA: ", responseJson) + + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null) { + let pathOnly = tmpView.split("?")[0]; + if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly; + + if (pathOnly === "/pricing" || pathOnly === "admin") { + window.location.replace(pathOnly + window.location.search); + } else { + if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) { + const redirectId = localStorage.getItem("redirectId") + localStorage.removeItem("redirectId") + pathOnly = pathOnly + "/" + redirectId + } + window.location.replace(pathOnly); + } + return + } + + //if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { + console.log("RUN Welcome!!") + //window.location.pathname = "/welcome?tab=2" + window.location.href = "/welcome" + }, 2000); + } + }), + ) + .catch(error => { + setLoginInfo("Error in login. Please try again, or contact support@shuffler.io if the problem persists.") + setLoginLoading(false) + }); + } + } + + const onChangeUser = (e) => { + setUsername(e.target.value) + } + + const onChangePass = (e) => { + setPassword(e.target.value) + } + + const HandleLoginWithSSO = () => { + setPassword("") + setLoginInfo("") + setLoginWithSSO(true) + } + + var formtitle = register ?
Welcome Back!
:
Create your account
+ var formButton = !isCloud ? "" : register ?
Don’t have an account yet?
Register here
: <> + +
Already have an account?
Login here
+ + //
Click here to Login
+ + // {formtitle} + + const buttonBackground = "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)" + + const buttonStyle = { borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(username, password) || loginLoading || (checkboxClicked && register) ? buttonBackground : "grey", color: "white" } + // +
+ +
+
+ {formButton} +
+
+ +
+ {loginInfo} +
+ + {( + ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length) > 0 + //|| (isCloud && !loginWithSSO && window?.location?.pathname !== "/register") + ? ( +
+ Or +
+ +
+
+ ) : null} + {isCloud && loginWithSSO && ( + + )} + + + + {isMobile ? null : ( + <> +
+ OR +
+ + + )} +
+ ); + + const loadedCheck = isLoaded ? +
+ {basedata} +
+ : +
+
+ + return ( +
+ {loadedCheck} +
+ ) +} + +export default LoginPage; diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx new file mode 100755 index 00000000..aaee73d6 --- /dev/null +++ b/frontend/src/views/LoginPageOld.jsx @@ -0,0 +1,554 @@ +/* eslint-disable react/no-multi-comp */ +import React, { useState, useEffect } from "react"; +import { makeStyles } from "@mui/styles"; +import { useInterval } from "react-powerhooks"; +import theme from '../theme.jsx'; + +import { + CircularProgress, + TextField, + Button, + Paper, + Typography, +} from "@mui/material"; + +import { useNavigate } from "react-router-dom"; + +const hrefStyle = { + color: "white", + textDecoration: "none", +}; + +const bodyDivStyle = { + margin: "auto", + marginTop: 150, + width: "500px", +}; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const LoginDialog = (props) => { + const { + globalUrl, + isLoaded, + isLoggedIn, + setIsLoggedIn, + setCookie, + register, + checkLogin, + } = props; + + let navigate = useNavigate(); + const classes = useStyles(); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [firstRequest, setFirstRequest] = useState(true); + const [loginLoading, setLoginLoading] = useState(false); + const [loginViewLoading, setLoginViewLoading] = useState(false); + const [ssoUrl, setSSOUrl] = useState(""); + + const [MFAField, setMFAField] = useState(false); + const [MFAValue, setMFAValue] = useState(""); + + + // Used to swap from login to register. True = login, false = register + + useEffect(() => { + checkAdmin() + }, [loginViewLoading]) + + // Error messages etc + const [loginInfo, setLoginInfo] = useState(""); + + const handleValidateForm = () => { + return username.length > 1 && password.length > 1; + }; + + if (isLoggedIn === true) { + //window.location.pathname = "/workflows"; + navigate("/workflows") + } + + const checkAdmin = () => { + const url = globalUrl + "/api/v1/checkusers"; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]) + + if (responseJson?.reason?.toLowerCase().includes("connection refused")) { + setLoginViewLoading(true) + start() + } + + } else { + + if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { + setSSOUrl(responseJson.sso_url); + } + + navigate("/login") + + if (loginViewLoading) { + setLoginViewLoading(false); + checkLogin(); + stop(); + + if ( + responseJson.reason !== undefined && + responseJson.reason !== null + ) { + setLoginInfo(responseJson.reason); + } + } + + if (responseJson.reason === "stay") { + navigate("/adminsetup") + } + } + }) + ) + .catch((error) => { + if (!loginViewLoading) { + setLoginViewLoading(true); + start(); + } + }); + }; + + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + checkAdmin(); + }, + }) + + if (firstRequest) { + setFirstRequest(false); + checkAdmin(); + } + + const onSubmit = (e) => { + setLoginLoading(true); + e.preventDefault(); + setLoginInfo(""); + // FIXME - add some check here ROFL + + // Just use this one? + var data = { username: username, password: password }; + if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { + data["mfa_code"] = MFAValue; + } + + var baseurl = globalUrl; + if (register) { + var url = baseurl + "/api/v1/login"; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + setLoginLoading(false); + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + if (responseJson["reason"] === "MFA_REDIRECT") { + setLoginInfo( + "MFA required. Please enter the 6-digit code from your authenticator" + ); + setMFAField(true); + return; + } else if (responseJson["reason"] === "MFA_SETUP") { + window.location.href = `/login/${responseJson.url}/mfa-setup`; + return; + } + + setLoginInfo("Successful login, rerouting"); + for (var key in responseJson["cookies"]) { + setCookie( + responseJson["cookies"][key].key, + responseJson["cookies"][key].value, + { path: "/" } + ); + } + + if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { + console.log("RUN Welcome!!") + setTimeout(() => { + navigate("/welcome?tab=2") + },200) + // window.location.pathname = "" + return + } + + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null) { + //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` + const newUrl = `/${tmpView}` + window.location.pathname = newUrl + } else { + window.location.pathname = "/workflows" + } + + setIsLoggedIn(true); + } + }) + ) + .catch((error) => { + setLoginLoading(false); + setLoginInfo("Error logging in: " + error); + }); + } else { + url = baseurl + "/api/v1/users/register"; + fetch(url, { + method: "POST", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + setLoginInfo("Successful register!"); + } + }) + ) + .catch((error) => { + setLoginInfo("Error in from backend: ", error); + }); + } + }; + + const onChangeUser = (e) => { + setUsername(e.target.value); + }; + + const onChangePass = (e) => { + setPassword(e.target.value); + }; + + //const onClickRegister = () => { + // if (props.location.pathname === "/login") { + // window.location.pathname = "/register" + // } else { + // window.location.pathname = "/login" + // } + + // setLoginCheck(!register) + //} + + //var loginChange = register ? (

Want to register? Click here.

) : (

Go back to login? Click here.

); + var formtitle = register ?
Login
:
Register
; + const imgsize = 100; + const basedata = ( +
+ +
+ +
+ + {loginViewLoading ? ( +
+ + Waiting for the Shuffle database to become available. This may + take up to two minutes. + + + {loginInfo === undefined || + loginInfo === null || + loginInfo.length === 0 ? null : ( +
Database Response: {loginInfo}
+ )} + + + + + + Are you sure Shuffle is{" "} + + installed correctly + + ? + + + + 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 2Gb of RAM available:{" "} +
+
+ sudo chown -R 1000:1000 shuffle-database +
+ + 2. Disable memory swap on the host: +
+
+ sudo swapoff -a +
+ + 3. Restart the database: +
+
+ sudo docker restart shuffle-opensearch +
+
+ + Need help?{" "} + + Join the Discord! + + +
+ ) : ( +
+

{formtitle}

+ Username +
+ +
+ Password +
+ +
+ {MFAField === true ? ( +
+ 2-factor code + { + setMFAValue(event.target.value); + }} + /> +
+ ) : null} +
+ +
+
{loginInfo}
+ {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( +
+ Or +
+ +
+
+ ) : null} +
+ )} +
+
+ ); + + const loadedCheck = isLoaded ?
{basedata}
:
; + + useEffect(() => { + setTimeout(() => { + if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) { + //id="sso_button" + const ssoBtn = document.getElementById("sso_button"); + if (ssoBtn !== undefined && ssoBtn !== null) { + //console.log("SSO BTN: ", ssoBtn) + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + var tmpView = new URLSearchParams(cursearch).get("autologin"); + if (tmpView !== undefined && tmpView !== null) { + if (tmpView === "true") { + console.log("Tmp: ", tmpView) + ssoBtn.click() + } + } + } + } + }, 200); + }, [ssoUrl]) + + return
{loadedCheck}
; +}; + +export default LoginDialog; diff --git a/frontend/src/views/RunWorkflow.jsx b/frontend/src/views/RunWorkflow.jsx index 883051a3..a9a6cf5c 100644 --- a/frontend/src/views/RunWorkflow.jsx +++ b/frontend/src/views/RunWorkflow.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-multi-comp */ -import React, {useState, useEffect} from 'react'; +import React, {useState, useEffect, useContext} from 'react'; import ReactDOM from "react-dom" import ReactJson from "react-json-view-ssr"; @@ -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,7 +46,9 @@ import { LockOpen as LockOpenIcon, OpenInNew as OpenInNewIcon, Edit as EditIcon, + Polyline as PolylineIcon, } from '@mui/icons-material'; +import { Context } from '../context/ContextApi.jsx'; const hrefStyle = { color: "white", @@ -55,6 +59,7 @@ const hrefStyle = { const RunWorkflow = (defaultprops) => { const { globalUrl, userdata, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, register, serverside } = defaultprops; + const { supportEmail } = useContext(Context); let navigate = useNavigate(); const [_, setUpdate] = useState(""); // Used to force rendring, don't remove const [explorerUi, setExplorerUi] = useState(false) @@ -74,6 +79,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 +186,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, { @@ -436,7 +473,7 @@ const RunWorkflow = (defaultprops) => { } if (response.status === 401 || response.status === 403) { - toast("This Form is not available for you to run. If you this is an error, contact support@shuffler.io with a link to this form") + toast(`This Form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form`) } return response.json() @@ -462,6 +499,8 @@ const RunWorkflow = (defaultprops) => { } else { console.log("Started execution") + start() + setExecutionRunning(true); if (answer !== undefined && answer !== null) { console.log("Skipping start") } else { @@ -590,7 +629,7 @@ const RunWorkflow = (defaultprops) => { } if (response.status === 401 || response.status === 403) { - toast("This Form is not available to you. If you think this is an error, please contact support@shuffler.io with the URL.") + toast(`This Form is not available to you. If you think this is an error, please contact ${supportEmail} with the URL.`) } return response.json() @@ -875,6 +914,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 +1095,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 +1189,100 @@ const RunWorkflow = (defaultprops) => { No Forms Found - Every 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. - + ALL 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. +
} + + {workflows === undefined || workflows === null || workflows.length === 0 ? null : + 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 ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose Subflow '{data.name}' + + + }> + { + window.location.href = `/forms/${data.id}` + }} + value={data} + > + + {data.name} + + + ) + }} + renderInput={(params) => { + return ( +
+ +
+ ) + }} + /> + }
) } @@ -1197,6 +1327,7 @@ const RunWorkflow = (defaultprops) => {
{onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}> {workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ? null :
+ {/* {workflow.name} { left: 200, }} /> + */} {organization} @@ -1323,7 +1455,8 @@ const RunWorkflow = (defaultprops) => { })}
: - answer !== undefined && answer !== null ? null : + (answer !== undefined && answer !== null) || message !== "" ? null : + Runtime Argument
@@ -1332,7 +1465,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", @@ -1373,9 +1512,11 @@ const RunWorkflow = (defaultprops) => { {message}. You may close this window. : - - {disabledButtons ? "Answered. You may close this window." : ""} - + + + {disabledButtons ? "Answered. You may close this window." : ""} + + } {disabledButtons ? null : @@ -1385,25 +1526,47 @@ const RunWorkflow = (defaultprops) => { }
- + onSubmit(null, execution_id, authorization, true) + }}> + Continue  or  - + onSubmit(null, execution_id, authorization, false) + }}> + Stop +
: @@ -1414,6 +1577,9 @@ const RunWorkflow = (defaultprops) => { color="primary" fullWidth disabled={!handleValidateForm(executionArgument) || executionLoading} + style={{ + textTransform: "none", + }} > {executionLoading ? @@ -1599,7 +1765,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") }} @@ -1612,7 +1781,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) }} @@ -1634,7 +1806,9 @@ const RunWorkflow = (defaultprops) => { disabled={workflow.id === undefined || workflow.id === null} variant={"contained"} color={"primary"} - style={{}} + style={{ + textTransform: "none", + }} onClick={() => { setEditWorkflowModalOpen(true) }} diff --git a/frontend/src/views/SetAuthentication.jsx b/frontend/src/views/SetAuthentication.jsx index 63aec23c..2ec3e780 100755 --- a/frontend/src/views/SetAuthentication.jsx +++ b/frontend/src/views/SetAuthentication.jsx @@ -1,13 +1,14 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { Typography, CircularProgress } from "@mui/material"; import theme from '../theme.jsx'; import { red, } from "../views/AngularWorkflow.jsx" +import { Context } from "../context/ContextApi.jsx"; const SetAuthentication = (props) => { const { globalUrl } = props; - + const { supportEmail } = useContext(Context); var headers = { "Content-Type": "application/json", "Accept": "application/json", @@ -328,7 +329,7 @@ const SetAuthentication = (props) => { {failed ? "Failed auth. Error: " : ""} {response}

- {failed ? "If the error persists, try to use fewer scopes. Contact support@shuffler.io if you need further assistance, and include the current URL and a screenshot. You may now close this window." : ""} + {failed ? `If the error persists, try to use fewer scopes. Contact ${supportEmail} if you need further assistance, and include the current URL and a screenshot. You may now close this window.` : ""}
); diff --git a/frontend/src/views/SettingsPage.jsx b/frontend/src/views/SettingsPage.jsx index a507857f..ec5ba658 100755 --- a/frontend/src/views/SettingsPage.jsx +++ b/frontend/src/views/SettingsPage.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import { useNavigate } from "react-router-dom"; -import theme from '../theme.jsx'; +import {getTheme} from '../theme.jsx'; import { Grid, Typography, @@ -15,6 +15,7 @@ import { //import { useAlert import { ToastContainer, toast } from "react-toastify"; import "../codeeditor-index.css"; +import { Context } from "../context/ContextApi.jsx"; import { FileCopy, Visibility, VisibilityOff } from "@mui/icons-material"; import IconButton from "@mui/material/IconButton"; @@ -40,6 +41,8 @@ const Settings = (props) => { const [MFARequired, setMFARequired] = React.useState(false); const [image2FA, setImage2FA] = React.useState(""); const [value2FA, setValue2FA] = React.useState(""); + const {themeMode, supportEmail} = useContext(Context); + const theme = getTheme(themeMode); // const [file, setFile] = React.useState(""); // const [fileBase64, setFileBase64] = React.useState( @@ -83,7 +86,7 @@ const Settings = (props) => { const boxStyle = { flex: "1", - color: "white", + color: theme.palette.text.primary, position: "relative", marginLeft: "10px", marginRight: "10px", @@ -213,8 +216,8 @@ const Settings = (props) => { left: "50%", transform: "translate(-50%, -50%)", zIndex: "9999", - backgroundColor: "#1a1a1a", - color: "white", + backgroundColor: theme.palette.backgroundColor, + color: theme.palette.text.primary, padding: 20, borderRadius: 5, boxShadow: "0 0 10px rgba(0, 0, 0, 0.3)", @@ -223,7 +226,7 @@ const Settings = (props) => { }; const closeIconButtonStyling = { - color: "white", + color: theme.palette.text.primary, border: "none", backgroundColor: "transparent", marginLeft: "90%", @@ -243,7 +246,7 @@ const Settings = (props) => { width: "100%", fontSize: 16, backgroundColor: disabled ? "gray" : "red", - color: "white", + color: theme.palette.text.primary, cursor: disabled === false && "pointer", }; const checkboxStyle = { @@ -381,7 +384,7 @@ const Settings = (props) => { className="ais-RefinementList-checkbox" onClick={handleCheckBoxEvent} /> -
@@ -410,7 +413,7 @@ const Settings = (props) => { endAdornment: ( {showPassword ? : } @@ -455,7 +458,12 @@ const Settings = (props) => { if (responseJson["success"] === false) { setPasswordFormMessage(responseJson["reason"]); } else { - toast("Changed password!"); + var reason = "" + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + reason += responseJson.reason + } + + toast.success("Changed password! " + reason); setPasswordFormMessage(""); } }) @@ -527,6 +535,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 +548,7 @@ const Settings = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); - } + } return response.json(); }) @@ -702,7 +712,7 @@ const Settings = (props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, }, }} color="primary" @@ -730,7 +740,7 @@ const Settings = (props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, }, }} color="primary" @@ -755,7 +765,7 @@ const Settings = (props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, }, }} color="primary" @@ -786,7 +796,7 @@ const Settings = (props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, }, }} color="primary" @@ -804,7 +814,7 @@ const Settings = (props) => { InputProps={{ style: { height: "50px", - color: "white", + color: theme.palette.text.primary, }, endAdornment: ( <> @@ -836,7 +846,12 @@ const Settings = (props) => { variant="outlined" />
+ */}
<>
diff --git a/frontend/src/views/Usecases2.jsx b/frontend/src/views/Usecases2.jsx index 886427b3..036c26a4 100644 --- a/frontend/src/views/Usecases2.jsx +++ b/frontend/src/views/Usecases2.jsx @@ -9,7 +9,7 @@ import { Context } from "../context/ContextApi.jsx" import { ToastContainer, toast } from "react-toastify" import { makeStyles, } from "@mui/styles" import classNames from "classnames" -import theme from '../theme.jsx' +import {getTheme} from '../theme.jsx' import { Autocomplete, @@ -123,8 +123,16 @@ const ParseUsecaseDesc = (priority, appFramework) => { const UsecaseListComponent = (props) => { const { keys, userdata, isCloud, globalUrl, frameworkData, isLoggedIn, workflows, setWorkflows, getFramework, setFrameworkData, } = props + const { themeMode, brandName } = useContext(Context) + const theme = getTheme(themeMode) - + const usecaseLightThemeColor = { + "collect": "#FB47A0", + "enrich": "#F38B14", + "detect": "#0AAD65", + "respond": "#289BDB", + "verify": "#624CE9", + } const [expandedIndex, setExpandedIndex] = useState(-1); const [expandedItem, setExpandedItem] = useState(-1); const [inputUsecase, setInputUsecase] = useState({}); @@ -199,8 +207,8 @@ const UsecaseListComponent = (props) => { const LoadingSkeleton = () => (
{/* Header skeleton */} - - + + {/* Apps selection skeleton */} @@ -220,7 +228,7 @@ const UsecaseListComponent = (props) => { variant="circular" width={40} height={40} - sx={{ bgcolor: 'grey.800' }} + sx={{ bgcolor: theme.palette.loaderColor}} /> ))}
@@ -232,7 +240,7 @@ const UsecaseListComponent = (props) => { sx={{ marginTop: "10px", borderRadius: 20, - bgcolor: 'grey.800' + bgcolor: theme.palette.loaderColor }} /> @@ -275,13 +283,13 @@ const UsecaseListComponent = (props) => { variant="circular" width={30} height={30} - sx={{ bgcolor: 'grey.800' }} + sx={{ bgcolor: theme.palette.loaderColor}} />
{/* Usecase title */} @@ -289,7 +297,7 @@ const UsecaseListComponent = (props) => { variant="text" width="70%" height={24} - sx={{ bgcolor: 'grey.800' }} + sx={{ bgcolor: theme.palette.loaderColor}} /> @@ -596,10 +604,10 @@ const UsecaseListComponent = (props) => { return (
- - Usecases + + Usecases - + Choose a template tailored to your automation requirements, ready for immediate use. @@ -742,7 +750,7 @@ const UsecaseListComponent = (props) => { {keys.map((usecase, index) => { return (
- + {index+1}. {usecase.name.slice(3, 100)} @@ -887,6 +895,9 @@ const Usecases2 = (props) => { const [keys, setKeys] = useState([]) const [treeKeys, setTreeKeys] = useState([]) + const { themeMode, brandName } = useContext(Context) + const theme = getTheme(themeMode) + const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState(""); const [selectedUsecases, setSelectedUsecases] = useState([]); const [usecases, setUsecases] = useState([]) @@ -987,7 +998,7 @@ const Usecases2 = (props) => { } - document.title = "Shuffle - usecases"; + document.title = brandName?.length > 0 ? `${brandName} - usecases` : "Shuffle - usecases"; var dayGraphLabels = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130]; var dayGraphData = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130]; diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 1eee8317..3fe551f4 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -440,6 +440,10 @@ export const collapseField = (field, inputdata) => { return true } + if (field.name === "result") { + return false + } + if (field.type === "array") { return true } @@ -454,6 +458,112 @@ export const collapseField = (field, inputdata) => { return false } +export const HandleJsonCopy = (base, copy, base_node_name) => { + if (typeof copy.name === "string") { + copy.name = copy.name.replaceAll(" ", "_"); + } + + //lol + if (typeof base === 'object' || typeof base === 'dict') { + base = JSON.stringify(base) + } + + if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") { + base_node_name = "exec" + } + + //console.log("COPY: ", base_node_name, copy); + + //var newitem = JSON.parse(base); + var newitem = validateJson(base).result + + var to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_"); + for (let copykey in copy.namespace) { + if (copy.namespace[copykey].includes("Results for")) { + continue; + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.namespace[copykey]]; + if (!isNaN(copy.namespace[copykey])) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.namespace[copykey]; + } + } + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.name]; + if (!isNaN(copy.name)) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.name; + } + } + + to_be_copied = to_be_copied.replaceAll(" ", "_"); + console.log("COPY: ", to_be_copied); + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + //console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(to_be_copied); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + //console.log("COPYING!"); + toast("Copied JSON path to clipboard.") + } else { + console.log("Couldn't find element ", elementName); + } +} + +export const handleReactJsonClipboard = (copy) => { + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + if ( + copy.namespace !== undefined && + copy.name !== undefined && + copy.src !== undefined + ) { + copy = copy.src; + } + + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + var stringified = JSON.stringify(copy); + if (stringified.startsWith('"') && stringified.endsWith('"')) { + stringified = stringified.substring(1, stringified.length - 1); + } + + navigator.clipboard.writeText(stringified); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + console.log("COPYING!"); + toast("Copied value to clipboard, NOT json path.") + } else { + console.log("Failed to copy from " + elementName + ": ", copyText); + } +} + export const validateJson = (showResult) => { if (showResult === undefined || showResult === null) { return { @@ -1946,7 +2056,10 @@ const Workflows = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); - toast("Failed deleting workflow. Do you have access?"); + + if (bulk !== true) { + toast(`Failed deleting workflow ${id}. Do you have access?`); + } } else { if (bulk !== true) { toast(`Deleted workflow ${id}. Child Workflows in Suborgs were also removed.`) diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index 33f8618f..d702f24f 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useContext, memo, useState, useRef } from "react"; import { useLocation, useNavigate, Link } from "react-router-dom"; import ReactDOM from "react-dom" +import { getTheme } from "../theme.jsx"; // Material UI Icons import Add from '@mui/icons-material/Add'; @@ -93,6 +94,11 @@ import { Visibility as VisibilityIcon, EditNote as EditNoteIcon, ErrorOutline as ErrorOutlineIcon, + Coronavirus as CoronavirusIcon, + Fingerprint as FingerprintIcon, + Psychology as PsychologyIcon, + Wifi as WifiIcon, + Devices as DevicesIcon, } from "@mui/icons-material"; // Additional Components @@ -103,7 +109,7 @@ import Dropzone from "../components/Dropzone.jsx"; import { ToastContainer, toast } from "react-toastify" import { MuiChipsInput } from "mui-chips-input"; import { v4 as uuidv4 } from "uuid"; -import theme from "../theme.jsx"; +// import theme from "./theme.jsx"; import algoliasearch from 'algoliasearch/lite'; import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinementList } from 'react-instantsearch-dom'; import { debounce } from "lodash"; @@ -112,43 +118,12 @@ import { removeQuery } from "../components/ScrollToTop.jsx"; import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx" -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240"); +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e"); const svgSize = 24; const imagesize = 22; -const useStyles = makeStyles(() => { - return { - datagrid: { - border: 0, - "& .MuiDataGrid-columnsContainer": { - backgroundColor: - theme?.palette?.type === "light" ? "#fafafa" : theme?.palette?.inputColor, - }, - "& .MuiDataGrid-iconSeparator": { - display: "none", - }, - "& .MuiDataGrid-colCell, .MuiDataGrid-cell": { - borderRight: `1px solid ${theme?.palette?.type === "light" ? "white" : "#303030" - }`, - }, - "& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell": { - borderBottom: `1px solid ${theme?.palette?.type === "light" ? "#f0f0f0" : "#303030" - }`, - }, - "& .MuiDataGrid-cell": { - color: - theme?.palette?.type === "light" ? "white" : "rgba(255,255,255,0.65)", - }, - "& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption": - { - borderRadius: 0, - color: "white", - }, - }, - } -}) @@ -160,19 +135,20 @@ const useStyles = makeStyles(() => { export const GetIconInfo = (action) => { // Finds the icon based on the action. Should be verbs. const iconList = [ - { key: "cases", values: ["cases"] }, + { key: "cases", values: ["cases", "ticket", "alert"] }, { key: "cache_add", values: ["set_cache"] }, { key: "cache_get", values: ["get_cache"] }, { key: "filter", values: ["filter"] }, { key: "merge", values: ["join", "merge", "route", "router"] }, { key: "search", - values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"], + values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate", "siem", ], }, { key: "list", values: ["list", "head", "options"] }, { key: "download", values: [ + "ingest", "capture", "get", "download", @@ -186,20 +162,6 @@ export const GetIconInfo = (action) => { }, { key: "add", values: ["add", "accept",] }, { key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] }, - { - key: "send", - values: [ - "send", - "dispatch", - "mail", - "forward", - "post", - "submit", - "mark", - "set", - "release", - ], - }, { key: "repeat", values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",], @@ -229,8 +191,29 @@ export const GetIconInfo = (action) => { key: "compare", values: ["compare", "convert", "to", "filter", "translate", "parse"], }, + { key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices"] }, { key: "close", values: ["close", "stop", "cancel", "block"] }, { key: "communication", values: ["communication", "comms", "email", "mail",] }, + { key: "eradication", values: ["eradication", "edr", "xdr"] }, + { key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] }, + { key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] }, + { key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] }, + { + key: "send", + values: [ + "send", + "dispatch", + "mail", + "forward", + "post", + "submit", + "mark", + "set", + "release", + ], + }, + + ]; var selectedKey = "" @@ -389,7 +372,45 @@ export const GetIconInfo = (action) => { iconBackgroundColor: "green", originalIcon: , }, - }; + eradication: { + icon: "", + iconColor: "white", + iconBackgroundColor: "green", + originalIcon: , + }, + iam: { + icon: "", + iconColor: "white", + iconBackgroundColor: "green", + originalIcon: , + }, + intel: { + icon: "", + iconColor: "white", + iconBackgroundColor: "green", + originalIcon: , + }, + network: { + icon: "", + iconColor: "white", + iconBackgroundColor: "green", + originalIcon: , + }, + assets: { + icon: "", + iconColor: "white", + iconBackgroundColor: "green", + originalIcon: , + }, + } + + /* + { key: "eradication", values: ["eradication", "edr", "xdr"] }, + { key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] }, + { key: "intel", values: ["intel", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti."] }, + { key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] }, + { key: "siem", values: ["siem", "security information and event management", "security information and event management", "security information and event management"] }, + */ var selectedItem = parsedIcons[selectedKey]; if (selectedItem === undefined || selectedItem === null) { @@ -406,11 +427,8 @@ export const GetIconInfo = (action) => { selectedItem.iconBackgroundColor = defaultColor; } - if (selectedItem.icon === "" || selectedItem.icon === undefined) { - console.log( - `MISSING PATH FOR ${selectedKey} (find in scope): `, - selectedItem.originalIcon.type.type - ); + if ((selectedItem.icon === "" || selectedItem.icon === undefined) && (selectedItem.originalIcon === undefined || selectedItem.originalIcon === "")) { + console.log(`MISSING PATH FOR ${selectedKey} (find in scope): `, selectedItem.originalIcon.type.type) } if ( @@ -436,19 +454,7 @@ export const GetIconInfo = (action) => { return selectedItem; }; -const chipStyle = { - backgroundColor: "#2F2F2F", - marginRight: 5, - paddingLeft: 5, - paddingRight: 5, - height: 35, - cursor: "pointer", - borderColor: "#2F2F2F", - color: "#C8C8C8", - fontSize: "14px", - fontFamily: theme?.typography?.fontFamily, - borderRadius: "17.5px" -}; + export const collapseField = (field) => { if (field === undefined || field === null) { @@ -626,12 +632,14 @@ export const validateJson = (showResult) => { //Custom hook for handling styling of the dropzone const useDropzoneStyles = () => { const { leftSideBarOpenByClick } = useContext(Context); + const { themeMode, brandColor } = useContext(Context); + const theme = getTheme(themeMode, brandColor); return { paddingTop: 70, // minHeight: 1000, - backgroundColor: "#1A1A1A", - fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme.palette.backgroundColor, + fontFamily: theme.typography?.fontFamily, // maxWidth: window.innerWidth > 1366 ? 1366 : isMobile ? "100%" : 1200, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", @@ -661,15 +669,66 @@ const Workflows2 = (props) => { const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false); const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false); - const [view, setView] = useState("grid"); - const classes = useStyles(theme) + const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid"); const imgSize = 60; + const { themeMode, brandColor, brandName } = useContext(Context); + const theme = getTheme(themeMode, brandColor); + const chipStyle = { + backgroundColor: theme.palette.chipStyle.backgroundColor, + marginRight: 5, + paddingLeft: 5, + paddingRight: 5, + height: 35, + cursor: "pointer", + borderColor: theme.palette.chipStyle.borderColor, + color: theme.palette.chipStyle.color, + fontSize: "14px", + fontFamily: theme.typography?.fontFamily, + borderRadius: "17.5px" + }; + + const newStyles = makeStyles(() => { + + return { + datagrid: { + border: 0, + "& .MuiDataGrid-columnsContainer": { + backgroundColor: + theme.palette?.type === "light" ? "#fafafa" : theme.palette?.inputColor, + }, + "& .MuiDataGrid-iconSeparator": { + display: "none", + }, + "& .MuiDataGrid-colCell, .MuiDataGrid-cell": { + borderRight: `1px solid ${theme.palette?.type === "light" ? "white" : "#303030" + }`, + }, + "& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell": { + borderBottom: `1px solid ${theme.palette?.type === "light" ? "#f0f0f0" : "#303030" + }`, + }, + "& .MuiDataGrid-cell": { + color: + theme.palette?.type === "light" ? "white" : "rgba(255,255,255,0.65)", + }, + "& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption": + { + borderRadius: 0, + color: "white", + }, + }, + } + }) + + const classes = newStyles(theme); + const referenceUrl = globalUrl + "/api/v1/hooks/"; var upload = ""; const [workflows, setWorkflows] = React.useState([]); + const [backupWorkflows, setBackupWorkflows] = React.useState([]); const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [selectedUsecases, setSelectedUsecases] = React.useState([]); const [filteredWorkflows, setFilteredWorkflows] = React.useState([]); @@ -724,7 +783,9 @@ const Workflows2 = (props) => { const [apps, setApps] = React.useState([]); - document.title = "Shuffle - Workflows"; + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + document.title = brandName?.length > 0 ? `${brandName} - Workflows` : "Shuffle - Workflows"; useEffect(() => { const queryParams = new URLSearchParams(location.search); @@ -742,11 +803,18 @@ const Workflows2 = (props) => { const handleTabChange = (event, newValue) => { setCurrTab(newValue); + + //if (view === "list" && currTab > 0) { + // setView("grid") + // setUpdate(Math.random()) + //} + // Update URL query params based on tab index const tabMapping = { 0: 'org_workflows', 1: 'my_workflows', - 2: 'all_workflows' + 2: 'all_workflows', + 3: 'backup_apps', }; const queryParams = new URLSearchParams(location.search); queryParams.set('tab', tabMapping[newValue]); @@ -755,9 +823,6 @@ const Workflows2 = (props) => { navigate(`${location.pathname}?${queryParams.toString()}`); }; - - - const handleCreateWorkflow = () => { setModalOpen(true) setIsEditing(false) @@ -810,9 +875,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) @@ -1096,23 +1162,31 @@ const Workflows2 = (props) => { setSelectedWorkflowId(""); }} PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: 500, - padding: 50, - }, - }} + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: '440px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} > -
+
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}?
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working
@@ -2275,6 +2336,11 @@ const Workflows2 = (props) => { setOpen(false); setAnchorEl(null); }} + MenuListProps={{ + sx: { + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + } + }} > {isDistributed ? { : null} { event.stopPropagation() @@ -2309,20 +2375,20 @@ const Workflows2 = (props) => { { window.open(`/forms/${data.id}`, "_blank") }} key={"explore forms"} > - {"Create Form"} + {"Edit Form"} { sideloadWorkflow(data.id, "publish") @@ -2336,8 +2402,8 @@ const Workflows2 = (props) => { { sideloadWorkflow(data.id, "export", setOpen) @@ -2352,7 +2418,7 @@ const Workflows2 = (props) => { { duplicateWorkflow(data) @@ -2365,7 +2431,7 @@ const Workflows2 = (props) => { { setDeleteModalOpen(true); setSelectedWorkflowId(data.id); @@ -2482,7 +2548,7 @@ const Workflows2 = (props) => { return ( -
+
{selectedCategory !== "" ? @@ -2496,7 +2562,7 @@ const Workflows2 = (props) => { width: 3, backgroundColor: boxColor, borderRadius: "0 100px 0 0", - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, }} onClick={() => { addFilter(selectedCategory) @@ -2504,21 +2570,24 @@ const Workflows2 = (props) => { /> : null} + - -
{ - navigate("/admin") - }} - > - {image} -
-
+ {currTab === 2 ? null : + +
{ + navigate("/admin") + }} + > + {image} +
+
+ } { /* @@ -2558,7 +2627,7 @@ const Workflows2 = (props) => { Edit: {data.name} @@ -2585,22 +2654,31 @@ const Workflows2 = (props) => { } placement="right"> + style={{ + textDecoration: "none", + color: "inherit", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + maxWidth: "90%", + display: "block" + }} + target={currTab === 2 ? "_blank" : "_self"} + > {parsedName} @@ -2617,7 +2695,7 @@ const Workflows2 = (props) => { style={{ height: 24, width: 24, - filter: "brightness(0.6)", + filter: themeMode === "dark" ? "brightness(0.6)" : "brightness(0.9)", cursor: "pointer", }} onClick={() => { @@ -2774,7 +2852,7 @@ const Workflows2 = (props) => { overflow: "hidden", marginTop: 8, maxHeight: 35, - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, }} > {data.tags !== undefined && data.tags !== null @@ -2813,7 +2891,7 @@ const Workflows2 = (props) => { {(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ? -
+
{ : null} {(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ? - -
+ +
{
- : null} +
@@ -3037,7 +3115,7 @@ const Workflows2 = (props) => { data.status, ).then((response) => { if (response !== undefined) { - toast("Successfully imported " + data.name); + toast.success("Imported " + data.name); } }); } @@ -3227,6 +3305,7 @@ const Workflows2 = (props) => {
} + { return obj; }) + console.log("ROWS: ", rows) + workflowData = ( { ); } return ( -
+
{ onClick={() => { localStorage.setItem("view", "list"); setView("list"); + + setCurrTab(0) }} > @@ -3979,20 +4062,30 @@ const Workflows2 = (props) => { // } useEffect(() => { - if (currTab === 2) return; - if (userdata !== undefined && userdata !== null && filteredWorkflows.length > 0) { + if (filteredWorkflows.length > 0) { + var categoryWorkflows = [] if (currTab === 0) { categoryWorkflows = filteredWorkflows.filter(workflow => workflow?.org_id === userdata?.active_org?.id) - setOrgWorkflows(categoryWorkflows) + + if (categoryWorkflows.length === 0) { + setOrgWorkflows(filteredWorkflows) + } else { + setOrgWorkflows(categoryWorkflows) + } } else if (currTab === 1) { categoryWorkflows = filteredWorkflows.filter(workflow => workflow?.org_id === userdata?.active_org?.id && workflow?.owner === userdata?.id) setMyWorkflows(categoryWorkflows) } + setIsLoadingWorkflow(false); - } + } else { + if (currTab === 0 && workflows?.length > 0) { + setOrgWorkflows(workflows) + } + } }, [currTab, workflows, userdata, filteredWorkflows, filters]) @@ -4006,8 +4099,8 @@ const Workflows2 = (props) => { const iconButtonStyle = { - color: 'white', - backgroundColor: '#212121', + color: theme.palette.text.primary, + backgroundColor: theme.palette.platformColor, borderRadius: '4px', padding: "12px 16px", cursor: 'pointer', @@ -4107,20 +4200,20 @@ const Workflows2 = (props) => { maxWidth: "25%", minWidth: "25%", height: 47, - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }} InputProps={{ style: { - color: "white", + color: theme.palette.textFieldStyle.color, height: "100%", - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, placeholder: "Search Workflows", }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: '4px', - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, }, }} value={localQuery} @@ -4162,9 +4255,9 @@ const Workflows2 = (props) => { maxWidth: "25%", height: 47, borderRadius: 4, - backgroundColor: "#212121", - fontFamily: theme?.typography?.fontFamily, - color: "#FFFFFF", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + fontFamily: theme.typography?.fontFamily, + color: theme.palette.textFieldStyle.color, }} MenuProps={{ anchorOrigin: { @@ -4177,9 +4270,9 @@ const Workflows2 = (props) => { }, PaperProps: { style: { - backgroundColor: '#212121', - color: '#FFFFFF', - fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color, + fontFamily: theme.typography?.fontFamily, } } }} @@ -4215,7 +4308,7 @@ const Workflows2 = (props) => { ) } > - + All Usecases {items.map((usecase, index) => ( @@ -4228,13 +4321,13 @@ const Workflows2 = (props) => { '&:hover': { backgroundColor: '#3A3A3A', // Darker background on hover }, - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, fontSize: 16 }} > {usecase.label} ({usecase.count}) @@ -4247,7 +4340,7 @@ const Workflows2 = (props) => { const tabStyle = { textTransform: 'none', marginRight: 20, - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, fontSize: 16, borderBottom: "5px solid transparent", minHeight: "48px", @@ -4255,9 +4348,9 @@ const Workflows2 = (props) => { } const tabActive = { - borderBottom: "5px solid #FF8544", + borderBottom: `5px solid ${theme.palette.primary.main}`, borderRadius: "2px", - color: "#FF8544" + color: theme.palette.primary.main } @@ -4277,26 +4370,26 @@ const Workflows2 = (props) => { display: "flex", flexDirection: "column", width: "100%", - maxWidth: "70%", + maxWidth: isSafari ? "100%" : "70%", margin: "auto", }}> - + {currTab === 0 ? "Org" : currTab === 1 ? "Your" : "Discover"} Workflows -
+
handleTabChange(event, newTab)} style={{ - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, fontSize: 16, marginBottom: "-2px" }} TabIndicatorProps={{ style: { display: 'none' } }} > { marginRight: 0, ...(currTab === 2 ? tabActive : {}) }} + /> + + {backupWorkflows.length > 0 && + + } + + { + navigate("/forms") + }} + style={{ + ...tabStyle, + marginRight: 0, + marginLeft: 25, + ...(currTab === 4 ? tabActive : {}) + }} />
@@ -4336,16 +4453,17 @@ const Workflows2 = (props) => { minWidth: "25%", height: 43, maxHeight: "fit-content", - backgroundColor: "#212121", - zIndex: 1000, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + zIndex: 1000, + color: theme.palette.textFieldStyle.color }} disabled={currTab === 2} InputProps={{ style: { - color: "white", height: "fit-content", maxHeight: "fit-content", - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color }, placeholder: "Filter Workflows", // endAdornment: ( @@ -4368,7 +4486,8 @@ const Workflows2 = (props) => { '& .MuiOutlinedInput-root': { height: "fit-content", borderRadius: '4px', - backgroundColor: '#212121', + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.23)', }, @@ -4382,10 +4501,12 @@ const Workflows2 = (props) => { display: 'flex', flexWrap: 'wrap', gap: '4px', + fontSize: 18, padding: '4px 8px', alignItems: 'center', height: "fit-content", // Match height - backgroundColor: "#212121", + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + color: theme.palette.textFieldStyle.color }, // Rest of the styling remains the same... @@ -4433,8 +4554,9 @@ const Workflows2 = (props) => { maxWidth: "25%", height: 47, borderRadius: 4, - backgroundColor: "#212121", - fontFamily: theme?.typography?.fontFamily + fontSize: 18, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + fontFamily: theme.typography?.fontFamily, }} sx={{ '& .MuiOutlinedInput-root': { @@ -4469,12 +4591,12 @@ const Workflows2 = (props) => { removeFilter(filters.indexOf(usecase?.name.toLowerCase())) } }} - style={{ + sx={{ padding: "12px 16px", borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)", "&:hover": { backgroundColor: "rgba(255,255,255,0.1)" - } + }, }} >
{ style={{ padding: 0, marginRight: 8, - color: "rgba(255,255,255,0.7)" + color: theme.palette.textFieldStyle.color, }} />
{ @@ -4509,8 +4631,8 @@ const Workflows2 = (props) => { { onClick={() => navigate("/workflows/debug")} disabled={currTab === 2} > - + - + { const newView = view === "grid" ? "list" : "grid"; localStorage.setItem("workflowView", newView); setView(newView); + + if (view === "grid") { + setCurrTab(0) + } }} disabled={currTab === 2} > {view === "grid" ? - : - + : + } @@ -4577,7 +4703,7 @@ const Workflows2 = (props) => { > {submitLoading ? : - + } @@ -4596,7 +4722,7 @@ const Workflows2 = (props) => { disabled={isCloud || currTab === 2} onClick={() => exportAllWorkflows(workflows)} > - +
@@ -4604,17 +4730,16 @@ const Workflows2 = (props) => { variant="contained" color="primary" onClick={handleCreateWorkflow} + id="create_workflow_button" style={{ borderRadius: 4, flex: 0.8, textTransform: 'none', - backgroundColor: "#FF8544", - color: "#1A1A1A", - fontFamily: theme?.typography?.fontFamily, + fontFamily: theme.typography?.fontFamily, fontSize: 16, fontWeight: 500 }} - startIcon={} + startIcon={} > Create Workflow @@ -4645,9 +4770,33 @@ const Workflows2 = (props) => { paddingBottom: 40 }}> + {currTab !== 0 ? null : + orgWorkflows.length === 0 ? + + No workflows found in this org with the Org ID filter. If this is an error, please click the "List View" button at the top to see ALL available workflows ({workflows.length}). + : + orgWorkflows.map((data, index) => { + // Shouldn't be a part of this list + if (data.public === true) { + return null + } + // if (firstLoad) { + // workflowDelay += 75 + // } else { + // return + // } - {currTab === 0 && orgWorkflows.map((data, index) => { + return ( + + {/**/} + + {/**/} + + ) + })} + + {currTab === 3 && backupWorkflows.map((data, index) => { // Shouldn't be a part of this list if (data.public === true) { return null @@ -4669,24 +4818,29 @@ const Workflows2 = (props) => { })} { - currTab === 1 && myWorkflows.map((data, index) => { - if (data.public === true) { - return null - } + currTab !== 1 ? null : + myWorkflows.length === 0 ? + + No workflows found in this org for your user. + : + myWorkflows.map((data, index) => { + if (data.public === true) { + return null + } - // if (firstLoad) { - // workflowDelay += 75 - // } else { - // return - // } + // if (firstLoad) { + // workflowDelay += 75 + // } else { + // return + // } - return ( - - {/**/} - - {/**/} - - ) + return ( + + {/**/} + + {/**/} + + ) }) }
@@ -5048,8 +5202,9 @@ const Workflows2 = (props) => { + //isLoaded && isLoggedIn && workflowDone ? ( const loadedCheck = - isLoaded && isLoggedIn && workflowDone ? ( + workflowDone && isLoaded ? (
{/* @@ -5112,12 +5267,20 @@ const Workflows2 = (props) => { }} > - Loading Workflows + + Loading Workflows and Apps +
); + const safariStyle = { + transform: 'scale(0.7)', + transformOrigin: 'top', + width: '100%', + height: '100%', + } // Maybe use gridview or something, idk - return
{loadedCheck}
; + return
{loadedCheck}
; }; diff --git a/functions/extensions/aws-s3-lambda/README.md b/functions/extensions/aws-s3-lambda/README.md deleted file mode 100644 index f665851a..00000000 --- a/functions/extensions/aws-s3-lambda/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# AWS Lambda forwarder to Shuffle -This function is made to forward S3 notifications to Shuffle to run a workflow when an object is made or updated. diff --git a/functions/extensions/aws-s3-lambda/s3_function.py b/functions/extensions/aws-s3-lambda/s3_function.py deleted file mode 100644 index d83d6154..00000000 --- a/functions/extensions/aws-s3-lambda/s3_function.py +++ /dev/null @@ -1,22 +0,0 @@ -import json -import urllib.parse -import urllib3 -import os - -print('Loading function') - -def lambda_handler(event, context): - # Get the object from the event and show its content type - bucket = event['Records'][0]['s3']['bucket']['name'] - - webhook = os.environ.get("SHUFFLE_WEBHOOK") - if not webhook: - return "No webhook environment defined: SHUFFLE_WEBHOOK" - - http = urllib3.PoolManager() - ret = http.request('POST', webhook, body=json.dumps(event["Records"][0]).encode("utf-8")) - if ret.status != 200: - return "Bad status code for webhook: %d" % ret.status - - print("Status code: %d\nData: %s" % (ret.status, ret.data)) - return "Successfully started with data %s" % ret.data diff --git a/functions/extensions/cortex-responders/Shuffle/requirements.txt b/functions/extensions/cortex-responders/Shuffle/requirements.txt deleted file mode 100755 index 6aabc3cf..00000000 --- a/functions/extensions/cortex-responders/Shuffle/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -cortexutils -requests diff --git a/functions/extensions/cortex-responders/Shuffle/shuffle.json b/functions/extensions/cortex-responders/Shuffle/shuffle.json deleted file mode 100755 index db755b49..00000000 --- a/functions/extensions/cortex-responders/Shuffle/shuffle.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "Shuffle", - "version": "1.0", - "author": "@frikkylikeme", - "url": "https://github.com/frikky/shuffle", - "license": "AGPL-V3", - "description": "Execute a workflow in Shuffle", - "dataTypeList": ["thehive:case", "thehive:alert", "thehive:case_artifact"], - "command": "Shuffle/shuffle.py", - "baseConfig": "Shuffle", - "configurationItems": [ - { - "name": "url", - "description": "The URL to your shuffle instance", - "type": "string", - "multi": false, - "required": true, - "defaultValue": "https://shuffler.io" - }, - { - "name": "api_key", - "description": "The API key to your Shuffle user", - "type": "string", - "multi": false, - "required": true - }, - { - "name": "verifyssl", - "description": "Verify SSL certificate", - "type": "boolean", - "multi": false, - "required": true, - "defaultValue": true - }, - { - "name": "workflow_id", - "description": "The ID of the workflow to execute", - "type": "string", - "multi": false, - "required": true - } - ] -} diff --git a/functions/extensions/cortex-responders/Shuffle/shuffle.py b/functions/extensions/cortex-responders/Shuffle/shuffle.py deleted file mode 100755 index db343230..00000000 --- a/functions/extensions/cortex-responders/Shuffle/shuffle.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -#encoding: utf-8 - -from cortexutils.responder import Responder -import requests - -class Shuffle(Responder): - def __init__(self): - Responder.__init__(self) - self.api_key = self.get_param("config.api_key", "") - self.url = self.get_param("config.url", "") - self.workflow_id = self.get_param("config.workflow_id", "") - self.verify = self.get_param('config.verifyssl', True, None) - - def run(self): - Responder.run(self) - parsed_url = "%s/api/v1/workflows/%s/execute" % (self.url, self.workflow_id) - headers = { - "Authorization": "Bearer %s" % self.api_key, - "User-Agent": "Cortex-Analyzer" - } - requests.post(parsed_url, headers=headers,verify=self.verify) - - self.report({'message': 'message sent'}) - -if __name__ == '__main__': - Shuffle().run() diff --git a/functions/extensions/cortex-responders/Shuffle_Webhook/requirements.txt b/functions/extensions/cortex-responders/Shuffle_Webhook/requirements.txt deleted file mode 100755 index 6aabc3cf..00000000 --- a/functions/extensions/cortex-responders/Shuffle_Webhook/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -cortexutils -requests diff --git a/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.json b/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.json deleted file mode 100755 index a6d3e9d8..00000000 --- a/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "Shuffle_webhook", - "version": "1.0", - "author": "@azgaviperr", - "url": "https://github.com/frikky/shuffle", - "license": "AGPL-V3", - "description": "Execute a webhook in Shuffle", - "dataTypeList": ["thehive:case", "thehive:alert", "thehive:case_artifact"], - "command": "Shuffle_Webhook/shuffle_webhook.py", - "baseConfig": "Shuffle_Webhook", - "configurationItems": [ - { - "name": "webhook_url", - "description": "The URL to your shuffle instance", - "type": "string", - "multi": false, - "required": true - }, - { - "name": "verifyssl", - "description": "Verify SSL certificate", - "type": "boolean", - "multi": false, - "required": true, - "defaultValue": true - } - ] -} diff --git a/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.py b/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.py deleted file mode 100755 index daed501c..00000000 --- a/functions/extensions/cortex-responders/Shuffle_Webhook/shuffle_webhook.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -#encoding: utf-8 - -from cortexutils.responder import Responder -import requests - -class Shuffle(Responder): - def __init__(self): - Responder.__init__(self) - self.api_key = self.get_param("config.api_key", "") - self.webhook_url = self.get_param("config.webhook_url", "") - self.webhook_id = self.get_param("config.webhook_id", "") - self.verify = self.get_param('config.verifyssl', True, None) - self.data = self.get_param('data') - - def run(self): - Responder.run(self) - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "Cortex-Analyzer" - } - requests.post(self.webhook_url, headers=headers,verify=self.verify, json=self.data) - - self.report({'message': 'message sent'}) - -if __name__ == '__main__': - Shuffle().run() diff --git a/functions/extensions/elasticsearch/run.sh b/functions/extensions/elasticsearch/run.sh deleted file mode 100755 index 666b1d95..00000000 --- a/functions/extensions/elasticsearch/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ELASTICSEARCH_USERNAME=frikky -e ELASTICSEARCH_PASSWORD=likeme -e xpack.security.enabled=true docker.elastic.co/elasticsearch/elasticsearch:7.12.1 -#docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.12.1 -# -# -# -#echo "\nWaiting for 1.5 minute, then adding data" -#sleep 90 -#echo "\nSlept 90 seconds: ADDING DATA" -#curl -XPOST http://localhost:9200/_security/user/frikky -H "Content-Type: application/json" -d '{"enabled": true, "email": "frikky@shuffler.io"}' - -#curl -XPOST -u frikky:likeme http://localhost:9200/samples/_doc -H "Content-Type: application/json" -d '{"src": "122.14.137.67", "dst": "103.35.191.16", "message": "alert", "md5": "CAEF973033E593C625FB2AA34F7026DC", "sha256": "DB1AEC5222075800EDA75D7205267569679B424E5C58A28102417F46D3B5790D", "hits": 0}' -#echo -#curl -XPOST -u frikky:likeme http://localhost:9200/samples/_doc -H "Content-Type: application/json" -d '{"src": "134.119.219.71", "dst": "103.35.191.41", "message": "alert", "md5": "9498FF82A64FF445398C8426ED63EA5B", "sha256": "8B2E701E91101955C73865589A4C72999AEABC11043F712E05FDB1C17C4AB19A", "hits": 0}' -# -#echo -#curl -XPOST -u frikky:likeme http://localhost:9200/samples2/_doc -H "Content-Type: application/json" -d '{"src": "122.14.137.67", "dst": "103.35.191.16", "message": "alert", "md5": "CAEF973033E593C625FB2AA34F7026DC", "sha256": "DB1AEC5222075800EDA75D7205267569679B424E5C58A28102417F46D3B5790D"}' -#echo -#curl -XPOST -u frikky:likeme http://localhost:9200/samples2/_doc -H "Content-Type: application/json" -d '{"src": "134.119.219.71", "dst": "103.35.191.41", "message": "alert", "md5": "9498FF82A64FF445398C8426ED63EA5B", "sha256": "8B2E701E91101955C73865589A4C72999AEABC11043F712E05FDB1C17C4AB19A"}' -#echo diff --git a/functions/extensions/k8s/shuffle/.helmignore b/functions/extensions/k8s/shuffle/.helmignore deleted file mode 100755 index 0e8a0eb3..00000000 --- a/functions/extensions/k8s/shuffle/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/functions/extensions/k8s/shuffle/Chart.yaml b/functions/extensions/k8s/shuffle/Chart.yaml deleted file mode 100755 index bd42981f..00000000 --- a/functions/extensions/k8s/shuffle/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: shuffle -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -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 - -# 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" diff --git a/functions/extensions/k8s/shuffle/templates/NOTES.txt b/functions/extensions/k8s/shuffle/templates/NOTES.txt deleted file mode 100755 index eaba4460..00000000 --- a/functions/extensions/k8s/shuffle/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "shuffle.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "shuffle.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "shuffle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "shuffle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/functions/extensions/k8s/shuffle/templates/_helpers.tpl b/functions/extensions/k8s/shuffle/templates/_helpers.tpl deleted file mode 100755 index 66efef07..00000000 --- a/functions/extensions/k8s/shuffle/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "shuffle.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "shuffle.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "shuffle.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "shuffle.labels" -}} -helm.sh/chart: {{ include "shuffle.chart" . }} -{{ include "shuffle.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "shuffle.selectorLabels" -}} -app.kubernetes.io/name: {{ include "shuffle.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "shuffle.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "shuffle.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/functions/extensions/k8s/shuffle/templates/deployment.yaml b/functions/extensions/k8s/shuffle/templates/deployment.yaml deleted file mode 100755 index f2050d82..00000000 --- a/functions/extensions/k8s/shuffle/templates/deployment.yaml +++ /dev/null @@ -1,253 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.name }}frontend - namespace: {{ .Values.namespace | quote }} -spec: - replicas: 1 - selector: - matchLabels: - service: shuffle - app: shuffle-frontend - template: - metadata: - labels: - service: shuffle - app: shuffle-frontend - spec: - containers: - - name: shuffle-frontend - image: ghcr.io/frikky/shuffle-frontend:nightly - imagePullPolicy: {{ .Values.image.pullPolicy | quote }} - env: - - name: BACKEND_HOSTNAME - value: backend-service - - name: TZ - value: Asia/Shanghai - ports: - - name: http - containerPort: 80 - hostPort: 3001 - - name: https - containerPort: 443 - hostname: shuffle-frontend - restartPolicy: Always - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.name }}backend - namespace: {{ .Values.namespace | quote }} -spec: - replicas: 1 - selector: - matchLabels: - service: shuffle - app: shuffle-backend - template: - metadata: - labels: - service: shuffle - app: shuffle-backend - spec: - containers: - - name: shuffle-backend - image: ghcr.io/frikky/shuffle-backend:nightly - env: - - name: BACKEND_HOSTNAME - value: "backend-service" - - name: BACKEND_PORT - value: "5001" - - name: ENVIRONMENT_NAME - value: "Shuffle" - - name: HTTPS_PROXY - - name: HTTP_PROXY - - name: ORG_ID - value: "Shuffle" - - name: OUTER_HOSTNAME - value: "backend-service" - - name: SHUFFLE_APP_FORCE_UPDATE - value: "false" - - name: SHUFFLE_APP_HOTLOAD_FOLDER - value: "/shuffle-apps" - - name: SHUFFLE_APP_HOTLOAD_LOCATION - value: "/shuffle-apps" - - name: SHUFFLE_CONTAINER_AUTO_CLEANUP - value: "false" - - name: SHUFFLE_DEFAULT_APIKEY - - name: SHUFFLE_DEFAULT_PASSWORD - - name: SHUFFLE_DEFAULT_USERNAME - - name: SHUFFLE_DOWNLOAD_AUTH_BRANCH - - name: SHUFFLE_DOWNLOAD_AUTH_PASSWORD - - name: SHUFFLE_DOWNLOAD_AUTH_USERNAME - - name: SHUFFLE_DOWNLOAD_WORKFLOW_BRANCH - - name: SHUFFLE_DOWNLOAD_WORKFLOW_LOCATION - - name: SHUFFLE_DOWNLOAD_WORKFLOW_PASSWORD - - name: SHUFFLE_DOWNLOAD_WORKFLOW_USERNAME - - name: SHUFFLE_ELASTIC - value: "true" - - name: SHUFFLE_OPENSEARCH_APIKEY - - name: SHUFFLE_OPENSEARCH_CERTIFICATE_FILE - - name: SHUFFLE_OPENSEARCH_CLOUDID - - name: SHUFFLE_OPENSEARCH_PASSWORD - - name: SHUFFLE_OPENSEARCH_PROXY - - name: SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY - value: "true" - - name: SHUFFLE_OPENSEARCH_URL - value: http://opensearch-service:9200 - - name: SHUFFLE_OPENSEARCH_USERNAME - value: "" - - name: SHUFFLE_PASS_APP_PROXY - value: "FALSE" - - name: SHUFFLE_PASS_WORKER_PROXY - value: "FALSE" - volumeMounts: - - mountPath: /var/run/docker.sock - name: docker-sock - - mountPath: /shuffle-apps - name: shuffle-app-hotload-location - - mountPath: /shuffle-files - name: shuffle-file-location - hostname: shuffle-backend - volumes: - - name: docker-sock - hostPath: - path: /var/run/docker.sock - - name: shuffle-app-hotload-location - hostPath: - path: /data/kubernetes/shuffle-apps - type: DirectoryOrCreate - - name: shuffle-file-location - hostPath: - path: /data/kubernetes/shuffle-files - type: DirectoryOrCreate - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.name }}orborus - namespace: {{ .Values.namespace | quote }} -spec: - replicas: 1 - selector: - matchLabels: - service: shuffle - app: shuffle-orborus - template: - metadata: - labels: - service: shuffle - app: shuffle-orborus - spec: - containers: - - name: shuffle-orborus - image: ghcr.io/frikky/shuffle-orborus:nightly - env: - - name: RUNNING_MODE - value: kubernetes - - name: BASE_URL - value: http://backend-service:5001 - - name: CLEANUP - value: "false" - - name: DOCKER_API_VERSION - value: "1.40" - - name: ENVIRONMENT_NAME - value: Shuffle - - name: HTTPS_PROXY - - name: HTTP_PROXY - - name: ORG_ID - value: Shuffle - - name: SHUFFLE_APP_SDK_VERSION - value: 0.8.97 - - name: SHUFFLE_BASE_IMAGE_NAME - value: frikky - - name: SHUFFLE_BASE_IMAGE_REGISTRY - value: ghcr.io - - name: SHUFFLE_BASE_IMAGE_TAG_SUFFIX - value: "-0.8.80" - - name: SHUFFLE_ORBORUS_EXECUTION_TIMEOUT - value: "600" - - name: SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY - value: "50" - - name: SHUFFLE_PASS_WORKER_PROXY - value: "TRUE" - - name: SHUFFLE_WORKER_VERSION - value: nightly - - name: TZ - value: Asia/Shanghai - volumeMounts: - - mountPath: /var/run/docker.sock - name: docker-sock - hostname: shuffle-orborus - volumes: - - name: docker-sock - hostPath: - path: /var/run/docker.sock -#--- -#apiVersion: apps/v1 -#kind: Deployment -#metadata: -# name: {{ .Values.name }}opensearch -# namespace: {{ .Values.namespace | quote }} -#spec: -# replicas: 1 -# selector: -# matchLabels: -# service: shuffle -# app: shuffle-opensearch -# template: -# metadata: -# labels: -# service: shuffle -# app: shuffle-opensearch -# spec: -# nodeSelector: -# node.bdlab-venus.com/opensearch: available -# initContainers: -# - name: permissions-fix -# image: frikky/busybox -# #volumeMounts: -# # - name: opensearch-claim0 -# # mountPath: /usr/share/elasticsearch/data -# command: [ 'chown' ] -# args: [ '1000:1000', '/usr/share/elasticsearch/data' ] -# containers: -# - name: shuffle-opensearch -# image: opensearchproject/opensearch:1.0.1 -# env: -# - name: TZ -# value: Asia/Shanghai -# - name: bootstrap.memory_lock -# value: "false" -# - name: OPENSEARCH_JAVA_OPTS -# value: "-Xms1024m -Xmx1024m" -# - name: opendistro_security.disabled -# value: "true" -# - name: cluster.routing.allocation.disk.threshold_enabled -# value: "false" -# - name: cluster.name -# value: shuffle-cluster -# - name: node.name -# value: opensearch-service -# - name: discovery.seed_hosts -# value: opensearch-service -# - name: cluster.initial_master_nodes -# value: opensearch-service - # volumeMounts: - # - mountPath: /usr/share/opensearch/data - # name: opensearch-claim0 - #volumes: - # - name: opensearch-claim0 - # persistentVolumeClaim: - # claimName: opensearch-claim0 -# volumeMounts: -# - mountPath: /usr/share/opensearch/data -# readOnly: true -# name: db-location -# volumes: -# - name: db-location -# hostPath: -# path: /data/kubernetes/shuffle-opensearch -# type: DirectoryOrCreate diff --git a/functions/extensions/k8s/shuffle/templates/hpa.yaml b/functions/extensions/k8s/shuffle/templates/hpa.yaml deleted file mode 100755 index 49f84ca2..00000000 --- a/functions/extensions/k8s/shuffle/templates/hpa.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "shuffle.fullname" . }} - labels: - {{- include "shuffle.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "shuffle.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/functions/extensions/k8s/shuffle/templates/ingress.yaml b/functions/extensions/k8s/shuffle/templates/ingress.yaml deleted file mode 100755 index e35d2ae0..00000000 --- a/functions/extensions/k8s/shuffle/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "shuffle.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "shuffle.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/functions/extensions/k8s/shuffle/templates/persistent-volume.yaml b/functions/extensions/k8s/shuffle/templates/persistent-volume.yaml deleted file mode 100755 index 934c68e2..00000000 --- a/functions/extensions/k8s/shuffle/templates/persistent-volume.yaml +++ /dev/null @@ -1,24 +0,0 @@ -#apiVersion: v1 -#kind: PersistentVolume -#metadata: -# name: opensearch-claim0 -# labels: -# app: opensearch-claim0 -#spec: -# capacity: -# storage: "10G" -# volumeMode: Filesystem -# persistentVolumeReclaimPolicy: Retain -# storageClassName: local-storage -# accessModes: -# - "ReadWriteOnce" -# local: -# path: "/data/kubernetes/shuffle-opensearch" -# nodeAffinity: -# required: -# nodeSelectorTerms: -# - matchExpressions: -# - key: node.dollar.com/opensearch -# operator: In -# values: -# - available diff --git a/functions/extensions/k8s/shuffle/templates/persistentvolumeclaim.yaml b/functions/extensions/k8s/shuffle/templates/persistentvolumeclaim.yaml deleted file mode 100755 index 7d1736a4..00000000 --- a/functions/extensions/k8s/shuffle/templates/persistentvolumeclaim.yaml +++ /dev/null @@ -1,17 +0,0 @@ -#apiVersion: v1 -#kind: PersistentVolumeClaim -#metadata: -# name: opensearch-claim0 -# namespace: {{ .Values.namespace }} -# labels: -# app: opensearch-claim0 -#spec: -# selector: -# matchLabels: -# app: opensearch-claim0 -# accessModes: -# - ReadWriteOnce -# storageClassName: local-storage -# resources: -# requests: -# storage: 5Gi diff --git a/functions/extensions/k8s/shuffle/templates/service.yaml b/functions/extensions/k8s/shuffle/templates/service.yaml deleted file mode 100755 index 8fc78dfa..00000000 --- a/functions/extensions/k8s/shuffle/templates/service.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: backend-service - namespace: {{ .Values.namespace | quote }} -spec: - ports: - - name: "5001" - port: 5001 - targetPort: 5001 - selector: - app: shuffle-backend - ---- -apiVersion: v1 -kind: Service -metadata: - name: frontend-service - namespace: {{ .Values.namespace | quote }} -spec: - type: NodePort - externalTrafficPolicy: Local - ports: - - name: "3001" - port: 3001 - nodePort: 3001 - targetPort: 80 - - name: "3443" - port: 3443 - nodePort: 3443 - targetPort: 443 - protocol: TCP - selector: - app: shuffle-frontend - -#--- -#apiVersion: v1 -#kind: Service -#metadata: -# name: opensearch-service -# namespace: {{ .Values.namespace | quote }} -#spec: -# type: NodePort -# externalTrafficPolicy: Local -# ports: -# - name: "9200" -# port: 9200 -# targetPort: 9200 -# nodePort: 9200 -# protocol: TCP -# selector: -# app: shuffle-opensearch diff --git a/functions/extensions/k8s/shuffle/templates/serviceaccount.yaml b/functions/extensions/k8s/shuffle/templates/serviceaccount.yaml deleted file mode 100755 index 11e9bdb7..00000000 --- a/functions/extensions/k8s/shuffle/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "shuffle.serviceAccountName" . }} - labels: - {{- include "shuffle.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/functions/extensions/k8s/shuffle/templates/tests/test-connection.yaml b/functions/extensions/k8s/shuffle/templates/tests/test-connection.yaml deleted file mode 100755 index e68f3142..00000000 --- a/functions/extensions/k8s/shuffle/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "shuffle.fullname" . }}-test-connection" - labels: - {{- include "shuffle.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "shuffle.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/functions/extensions/k8s/shuffle/values.yaml b/functions/extensions/k8s/shuffle/values.yaml deleted file mode 100755 index 07e935c6..00000000 --- a/functions/extensions/k8s/shuffle/values.yaml +++ /dev/null @@ -1,82 +0,0 @@ -# Default values for shuffle. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: nginx - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 80 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/functions/extensions/misp/Dockerfile b/functions/extensions/misp/Dockerfile deleted file mode 100755 index f2b8ae3f..00000000 --- a/functions/extensions/misp/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM python:3.9.4-alpine as base - -FROM base as builder - -RUN mkdir /install -WORKDIR /install - -FROM base -RUN apk add g++ - -COPY --from=builder /install /usr/local -COPY requirements.txt /requirements.txt -RUN pip3 install -r /requirements.txt - - -RUN mkdir /app -WORKDIR /app -COPY requirements.txt /app/requirements.txt -RUN python3 -m pip install -r /app/requirements.txt - -COPY sub.py /app/sub.py - -CMD ["python3", "sub.py"] diff --git a/functions/extensions/misp/docker-compose.yml b/functions/extensions/misp/docker-compose.yml deleted file mode 100755 index 5b0130c0..00000000 --- a/functions/extensions/misp/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3' -services: - zmq: - image: ghcr.io/frikky/shuffle-zmq:latest - environment: - - ZMQ_HOSTNAME=localhost - - ZMQ_PORT=50000 - - ZMQ_FORWARD_URL=https://shuffler.io/api/v1/hooks/webhook_e09bea36-9976-1421-82bc-b8764ca83c1e - restart: unless-stopped diff --git a/functions/extensions/misp/requirements.txt b/functions/extensions/misp/requirements.txt deleted file mode 100755 index a1cdf7dd..00000000 --- a/functions/extensions/misp/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyzmq -requests diff --git a/functions/extensions/misp/sub.py b/functions/extensions/misp/sub.py deleted file mode 100755 index feb78c08..00000000 --- a/functions/extensions/misp/sub.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -print("Running imports") -import sys -import zmq -import json -import time -import pprint -import os -import sys -import requests - -forward_url = os.getenv("ZMQ_FORWARD_URL", "") -print("Checking forward url (ZMQ_FORWARD_URL): %s" % forward_url) -def handle_hook(data): - ret = requests.post(forward_url, json=data) - print(ret.text) - print(ret.status_code) - -def main(): - host = os.getenv("ZMQ_HOST", "localhost") - port = os.getenv("ZMQ_PORT", "50000") - - if len(forward_url) == 0: - print("Failed to start - define ZMQ_FORWARD_URL for webhook forwarder") - exit(0) - - print("Starting connection setup to %s:%s" % (host, port)) - context = zmq.Context() - socket = context.socket(zmq.SUB) - socket.connect ("tcp://%s:%s" % (host, port)) - socket.setsockopt(zmq.SUBSCRIBE, b'') - - poller = zmq.Poller() - poller.register(socket, zmq.POLLIN) - - print("Starting zmq check for %s:%s" % (host, port)) - while True: - socks = dict(poller.poll(timeout=None)) - if socket in socks and socks[socket] == zmq.POLLIN: - message = socket.recv() - #print(message) - topic, s, m = message.decode('utf-8').partition(" ") - - d = json.loads(m) - try: - # print test if you want status (heartbeat) - test = d["status"] - except KeyError: - handle_hook(d) - - time.sleep(1) - -if __name__ == "__main__": - print("In init ") - main() diff --git a/functions/extensions/scripts/disable_disk_check.sh b/functions/extensions/scripts/disable_disk_check.sh deleted file mode 100755 index cadda899..00000000 --- a/functions/extensions/scripts/disable_disk_check.sh +++ /dev/null @@ -1,11 +0,0 @@ -curl -XPUT -u admin:admin https://localhost:9200/_cluster/settings -H "Content-Type:application/json" -k -d \ -'{ - "transient": { - "cluster.routing.allocation.disk.threshold_enabled": false - } -}' - -curl -XPUT -u admin:admin https://localhost:9200/_all/_settings -H "Content-Type: application/json" -k -d \ -'{ - "index.blocks.read_only_allow_delete": null -}' diff --git a/functions/extensions/swarm/.gitignore b/functions/extensions/swarm/.gitignore deleted file mode 100644 index 9ce4a537..00000000 --- a/functions/extensions/swarm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -shuffle-database/nodes diff --git a/functions/extensions/swarm/docker-compose.yml b/functions/extensions/swarm/docker-compose.yml deleted file mode 100644 index 57ea9b49..00000000 --- a/functions/extensions/swarm/docker-compose.yml +++ /dev/null @@ -1,131 +0,0 @@ -version: '3.4' -services: - backend: - image: ghcr.io/frikky/shuffle-backend:nightly - #hostname: shuffle-backend - environment: - BACKEND_HOSTNAME: backend - OUTER_HOSTNAME: backend - BACKEND_PORT: '5001' - HTTPS_PROXY: '' - HTTP_PROXY: '' - SHUFFLE_APP_DOWNLOAD_LOCATION: https://github.com/frikky/shuffle-apps - SHUFFLE_APP_FORCE_UPDATE: 'false' - SHUFFLE_APP_HOTLOAD_FOLDER: /shuffle-apps - SHUFFLE_APP_HOTLOAD_LOCATION: ./shuffle-apps - DATASTORE_EMULATOR_HOST: "shuffle-database:8000" - DOCKER_API_VERSION: '1.40' - SHUFFLE_BASE_IMAGE_NAME: frikky - SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io - SHUFFLE_BASE_IMAGE_TAG_SUFFIX: '-0.9.30' - SHUFFLE_CONTAINER_AUTO_CLEANUP: 'true' - SHUFFLE_DEFAULT_APIKEY: '' - SHUFFLE_FILE_LOCATION: /shuffle-files - SHUFFLE_OPENSEARCH_APIKEY: '' - SHUFFLE_OPENSEARCH_CERTIFICATE_FILE: '' - SHUFFLE_OPENSEARCH_CLOUDID: '' - SHUFFLE_OPENSEARCH_PROXY: '' - SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY: 'true' - SHUFFLE_OPENSEARCH_URL: http://opensearch:9200 - SHUFFLE_PASS_APP_PROXY: 'FALSE' - SHUFFLE_PASS_WORKER_PROXY: 'TRUE' - SHUFFLE_ELASTIC: 'true' - #SHUFFLE_ENCRYPTION_MODIFIER: - ports: - - "5001:5001" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./shuffle-apps:/shuffle-apps - - ./shuffle-files:/shuffle-files - networks: - - shuffle_prod - #- reverseproxy - depends_on: - - opensearch - logging: - driver: json-file - frontend: - image: ghcr.io/frikky/shuffle-frontend:nightly - healthcheck: - test: curl -fs http://localhost:80 || exit 1 - interval: 30s - timeout: 5s - retries: 3 - ports: - - "3001:80" - - "3443:443" - networks: - - shuffle_prod - #- reverseproxy - environment: - - "BACKEND_HOSTNAME=backend" - depends_on: - - backend - deploy: - update_config: - order: start-first - opensearch: - image: opensearchproject/opensearch:1.1.0 - healthcheck: - test: curl -fs http://localhost:9200/_cat/health || exit 1 - interval: 30s - timeout: 5s - retries: 3 - environment: - - bootstrap.memory_lock=false - - "OPENSEARCH_JAVA_OPTS=-Xms1024m -Xmx1024m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM - - plugins.security.disabled=true - - cluster.routing.allocation.disk.threshold_enabled=false - - cluster.name=shuffle-cluster - - node.name=opensearch - - discovery.seed_hosts=opensearch - - cluster.initial_master_nodes=opensearch - - node.store.allow_mmap=false - volumes: - - ./shuffle-database:/usr/share/opensearch/data:rw - networks: - - shuffle_prod - #- reverseproxy - logging: - driver: json-file - - orborus: - image: ghcr.io/frikky/shuffle-orborus:nightly - #hostname: shuffle-orborus - environment: - #SHUFFLE_WORKER_VERSION: nightly - SHUFFLE_APP_SDK_VERSION: 0.8.97 - SHUFFLE_WORKER_VERSION: nightly - BASE_URL: http://backend:5001 - #BASE_URL: http://192.168.86.37:5001 - CLEANUP: 'true' - DOCKER_API_VERSION: '1.40' - ENVIRONMENT_NAME: Shuffle - HTTPS_PROXY: '' - HTTP_PROXY: '' - ORG_ID: Shuffle - SHUFFLE_BASE_IMAGE_NAME: frikky - SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io - SHUFFLE_BASE_IMAGE_TAG_SUFFIX: -0.8.80 - SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY: '50' - SHUFFLE_ORBORUS_EXECUTION_TIMEOUT: '800' - SHUFFLE_PASS_APP_PROXY: 'FALSE' - SHUFFLE_PASS_WORKER_PROXY: 'TRUE' - SHUFFLE_SCALE_REPLICAS: 5 - SHUFFLE_SWARM_NETWORK_NAME: shuffle_prod - SHUFFLE_SWARM_CONFIG: "run" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - networks: - - shuffle_prod - #- reverseproxy - logging: - driver: json-file - -networks: - shuffle_prod: - driver: overlay - external: true - #reverseproxy: - # driver: overlay - # #external: true diff --git a/functions/extensions/swarm/network.sh b/functions/extensions/swarm/network.sh deleted file mode 100644 index f3a323ed..00000000 --- a/functions/extensions/swarm/network.sh +++ /dev/null @@ -1 +0,0 @@ -docker network create -d overlay shuffle_prod diff --git a/functions/extensions/swarm/orborus.yml b/functions/extensions/swarm/orborus.yml deleted file mode 100644 index fb90b10f..00000000 --- a/functions/extensions/swarm/orborus.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '3.4' -services: - orborus: - image: ghcr.io/frikky/shuffle-orborus:nightly - #hostname: shuffle-orborus - environment: - #SHUFFLE_WORKER_VERSION: nightly - SHUFFLE_APP_SDK_VERSION: 0.8.97 - SHUFFLE_WORKER_VERSION: nightly - BASE_URL: http://:5001 - #BASE_URL: http://192.168.86.37:5001 - CLEANUP: 'true' - DOCKER_API_VERSION: '1.40' - ENVIRONMENT_NAME: Shuffle - HTTPS_PROXY: '' - HTTP_PROXY: '' - ORG_ID: Shuffle - SHUFFLE_BASE_IMAGE_NAME: frikky - SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io - SHUFFLE_BASE_IMAGE_TAG_SUFFIX: -0.8.80 - SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY: '50' - SHUFFLE_ORBORUS_EXECUTION_TIMEOUT: '800' - SHUFFLE_PASS_APP_PROXY: 'FALSE' - SHUFFLE_PASS_WORKER_PROXY: 'TRUE' - SHUFFLE_SCALE_REPLICAS: 5 - SHUFFLE_SWARM_NETWORK_NAME: shuffle_prod - SHUFFLE_SWARM_CONFIG: "run" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - networks: - - shuffle_prod - #- reverseproxy - logging: - driver: json-file - -networks: - shuffle_prod: - driver: overlay - external: true diff --git a/functions/extensions/swarm/run.sh b/functions/extensions/swarm/run.sh deleted file mode 100644 index faeadab8..00000000 --- a/functions/extensions/swarm/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -docker swarm init -chown 1000:1000 -R shuffle-database/ -docker network create -d overlay shuffle_prod -docker stack deploy --compose-file=docker-compose.yml shuffle_swarm diff --git a/functions/extensions/swarm/run_orborus.sh b/functions/extensions/swarm/run_orborus.sh deleted file mode 100644 index bcbd626f..00000000 --- a/functions/extensions/swarm/run_orborus.sh +++ /dev/null @@ -1,4 +0,0 @@ -docker swarm init -chown 1000:1000 -R shuffle-database/ -docker network create -d overlay shuffle_prod -docker stack deploy --compose-file=orborus.yml shuffle_orborus diff --git a/functions/extensions/swarm/shuffle-apps/tmp b/functions/extensions/swarm/shuffle-apps/tmp deleted file mode 100644 index e69de29b..00000000 diff --git a/functions/extensions/swarm/shuffle-database/tmp b/functions/extensions/swarm/shuffle-database/tmp deleted file mode 100644 index e69de29b..00000000 diff --git a/functions/extensions/swarm/shuffle-files/tmp b/functions/extensions/swarm/shuffle-files/tmp deleted file mode 100644 index e69de29b..00000000 diff --git a/functions/extensions/swarm/stop.sh b/functions/extensions/swarm/stop.sh deleted file mode 100644 index c516f348..00000000 --- a/functions/extensions/swarm/stop.sh +++ /dev/null @@ -1,3 +0,0 @@ -docker stack rm shuffle_swarm - -# diff --git a/functions/extensions/wazuh/custom-shuffle b/functions/extensions/wazuh/custom-shuffle deleted file mode 100755 index bd540414..00000000 --- a/functions/extensions/wazuh/custom-shuffle +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -# Created by Shuffle, AS. . - -WPYTHON_BIN="framework/python/bin/python3" - -SCRIPT_PATH_NAME="$0" - -DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)" -SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})" - -case ${DIR_NAME} in - */active-response/bin | */wodles*) - if [ -z "${WAZUH_PATH}" ]; then - WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)" - fi - - PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py" - ;; - */bin) - if [ -z "${WAZUH_PATH}" ]; then - WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)" - fi - - PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py" - ;; - */integrations) - if [ -z "${WAZUH_PATH}" ]; then - WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)" - fi - - PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py" - ;; -esac - - -${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@" diff --git a/functions/extensions/wazuh/custom-shuffle.py b/functions/extensions/wazuh/custom-shuffle.py deleted file mode 100755 index 8e555ea9..00000000 --- a/functions/extensions/wazuh/custom-shuffle.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -# Created by Shuffle, AS. . -# Based on the Slack integration using Webhooks - -import json -import sys -import time -import os - -try: - import requests - from requests.auth import HTTPBasicAuth -except Exception as e: - print("No module 'requests' found. Install: pip install requests") - sys.exit(1) - -# ADD THIS TO ossec.conf configuration: -# -# custom-shuffle -# http://:3001/api/v1/hooks/ -# 3 -# json -# - -# Global vars -debug_enabled = False -pwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -json_alert = {} -now = time.strftime("%a %b %d %H:%M:%S %Z %Y") - -# Set paths -log_file = '{0}/logs/integrations.log'.format(pwd) - -try: - with open("/tmp/shuffle_start.txt", "w+") as tmp: - tmp.write("Script started") -except: - pass - - -def main(args): - debug("# Starting") - - # Read args - alert_file_location = args[1] - webhook = args[3] - - debug("# Webhook") - debug(webhook) - - debug("# File location") - debug(alert_file_location) - - # Load alert. Parse JSON object. - try: - with open(alert_file_location) as alert_file: - json_alert = json.load(alert_file) - except: - debug("# Alert file %s doesn't exist" % alert_file_location) - - debug("# Processing alert") - try: - debug(json_alert) - except Exception as e: - debug("Failed getting json_alert %s" % e) - sys.exit(1) - - debug("# Generating message") - msg = generate_msg(json_alert) - if isinstance(msg, str): - if len(msg) == 0: - return - debug(msg) - - debug("# Sending message") - - try: - with open("/tmp/shuffle_end.txt", "w+") as tmp: - tmp.write("Script done pre-msg sending") - except: - pass - - - send_msg(msg, webhook) - - -def debug(msg): - if debug_enabled: - msg = "{0}: {1}\n".format(now, msg) - print(msg) - f = open(log_file, "a") - f.write(msg) - f.close() - -# Skips container kills to stop self-recursion -def filter_msg(alert): - # These are things that recursively happen because Shuffle starts Docker containers - skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928", "5710"] - if alert["rule"]["id"] in skip: - return False - - #try: - # if "docker" in alert["rule"]["description"].lower() and " - #msg['text'] = alert.get('full_log') - #except: - # pass - #msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A" - - return True - -def generate_msg(alert): - if not filter_msg(alert): - print("Skipping rule %s" % alert["rule"]["id"]) - return "" - - level = alert['rule']['level'] - - if (level <= 4): - severity = 1 - elif (level >= 5 and level <= 7): - severity = 2 - else: - severity = 3 - - msg = {} - msg['severity'] = severity - msg['pretext'] = "WAZUH Alert" - msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A" - msg['text'] = alert.get('full_log') - msg['rule_id'] = alert["rule"]["id"] - msg['timestamp'] = alert["timestamp"] - msg['id'] = alert['id'] - msg["all_fields"] = alert - - #msg['fields'] = [] - # msg['fields'].append({ - # "title": "Agent", - # "value": "({0}) - {1}".format( - # alert['agent']['id'], - # alert['agent']['name'] - # ), - # }) - #if 'agentless' in alert: - # msg['fields'].append({ - # "title": "Agentless Host", - # "value": alert['agentless']['host'], - # }) - - #msg['fields'].append({"title": "Location", "value": alert['location']}) - #msg['fields'].append({ - # "title": "Rule ID", - # "value": "{0} _(Level {1})_".format(alert['rule']['id'], level), - #}) - - #attach = {'attachments': [msg]} - - return json.dumps(msg) - - -def send_msg(msg, url): - debug("# In send msg") - headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} - res = requests.post(url, data=msg, headers=headers, verify=False) - debug("# After send msg: %s" % res) - - -if __name__ == "__main__": - try: - # Read arguments - bad_arguments = False - if len(sys.argv) >= 4: - msg = '{0} {1} {2} {3} {4}'.format( - now, - sys.argv[1], - sys.argv[2], - sys.argv[3], - sys.argv[4] if len(sys.argv) > 4 else '', - ) - #debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug') - debug_enabled = True - else: - msg = '{0} Wrong arguments'.format(now) - bad_arguments = True - - # Logging the call - try: - f = open(log_file, 'a') - except: - f = open(log_file, 'w+') - f.write("") - f.close() - - f = open(log_file, 'a') - f.write(msg + '\n') - f.close() - - if bad_arguments: - debug("# Exiting: Bad arguments. Inputted: %s" % sys.argv) - sys.exit(1) - - # Main function - main(sys.argv) - - except Exception as e: - debug(str(e)) - raise diff --git a/functions/extensions/wazuh/ossec.conf b/functions/extensions/wazuh/ossec.conf deleted file mode 100755 index e87bc53d..00000000 --- a/functions/extensions/wazuh/ossec.conf +++ /dev/null @@ -1,6 +0,0 @@ - - custom-shuffle - 9 - http://:/api/v1/hooks/webhook_hookid - json - diff --git a/functions/kubernetes/charts/shuffle/Chart.yaml b/functions/kubernetes/charts/shuffle/Chart.yaml index f11f1726..0a4e4fa3 100644 --- a/functions/kubernetes/charts/shuffle/Chart.yaml +++ b/functions/kubernetes/charts/shuffle/Chart.yaml @@ -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 diff --git a/functions/kubernetes/charts/shuffle/README.md b/functions/kubernetes/charts/shuffle/README.md index 495d2690..6c3a4ed2 100644 --- a/functions/kubernetes/charts/shuffle/README.md +++ b/functions/kubernetes/charts/shuffle/README.md @@ -18,7 +18,7 @@ SPDX-License-Identifier: APACHE-2.0 ```sh # Install shuffle via helm (the shuffle namespace is hardcoded into the shuffle source code) -helm install shuffle oci://ghcr.io/shuffle/shuffle/charts/shuffle --namespace shuffle --create-namespace +helm install shuffle oci://ghcr.io/shuffle/charts/shuffle --namespace shuffle --create-namespace ``` Make sure that no other application is deployed to the shuffle namespace, as shuffle deletes kubernetes resources in this namespace. @@ -105,6 +105,15 @@ SHUFFLE_DEFAULT_APIKEY: "72E41083-A6F6-4A1B-8538-B06B577F47F0" # Shuffle uses uu SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" ``` +## OpenSearch + +Shuffle uses OpenSearch as its database. This helm chart installs a single-node OpenSearch cluster using [the Bitnami Helm Chart](https://github.com/bitnami/charts/blob/main/bitnami/opensearch/values.yaml). +You can customize the helm chart using the values of the Bitnami helm chart under the `opensearch` prefix (e.g. `opensearch.master.replicaCOunt`). + +Alternatively, you can disable the built-in OpenSearch installation using `opensearch.enabled=false`. +Provide your own OpenSearch url and username with `backend.openSearch.url` and `backend.openSearch.username`. +The password should be provided with the `SHUFFLE_OPENSEARCH_PASSWORD` env variable to the backend. + ## Parameters ### Global parameters @@ -148,6 +157,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 +194,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 +206,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` | `""` | @@ -209,9 +217,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `backend.affinity` | Affinity for backend pods assignment | `{}` | | `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.updateStrategy.type` | backend deployment strategy type | `Recreate` | | `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 | `""` | @@ -238,6 +244,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `backend.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` | | `backend.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` | | `backend.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` | +| `backend.service.labels` | Extra labels for backend service | `{}` | | `backend.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | | `backend.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | | `backend.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | @@ -265,6 +272,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 +310,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 +322,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 +334,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 | `""` | @@ -356,6 +360,7 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `frontend.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` | | `frontend.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` | | `frontend.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` | +| `frontend.service.labels` | Extra labels for frontend service | `{}` | | `frontend.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | | `frontend.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | | `frontend.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | @@ -373,11 +378,11 @@ 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 | `[]` | | `orborus.replicaCount` | Number of orborus replicas to deploy | `1` | -| `orborus.containerPorts.http` | orborus HTTP container port | `8080` | | `orborus.extraContainerPorts` | Optionally specify extra list of additional ports for orborus containers | `[]` | | `orborus.livenessProbe.enabled` | Enable livenessProbe on orborus containers | `false` | | `orborus.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | @@ -409,8 +414,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 +426,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 +438,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 | `""` | @@ -477,22 +478,38 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" ### worker Parameters -| Name | Description | Value | -| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | -| `worker.image.registry` | worker image registry | `ghcr.io` | -| `worker.image.repository` | worker image repository | `shuffle/shuffle-worker` | -| `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. | `""` | -| `worker.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | -| `worker.serviceAccount.automountServiceAccountToken` | Automount service account token for the worker service account | `true` | -| `worker.serviceAccount.imagePullSecrets` | Add image pull secrets to the worker service account | `[]` | -| `worker.rbac.create` | Specifies whether RBAC resources should be created | `true` | -| `worker.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | -| `worker.networkPolicy.allowExternal` | Don't require server label for connections | `true` | -| `worker.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | -| `worker.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | -| `worker.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | +| Name | Description | Value | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| `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.podSecurityContext.enabled` | Enable worker pods' Security Context | `true` | +| `worker.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for worker pods | `Always` | +| `worker.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for worker pods | `[]` | +| `worker.podSecurityContext.supplementalGroups` | Set filesystem extra groups for worker pods | `[]` | +| `worker.podSecurityContext.fsGroup` | Set fsGroup in worker pods' Security Context | `1001` | +| `worker.containerSecurityContext.enabled` | Enabled worker container' Security Context | `true` | +| `worker.containerSecurityContext.seLinuxOptions` | Set SELinux options in worker container | `{}` | +| `worker.containerSecurityContext.runAsUser` | Set runAsUser in worker container' Security Context | `1001` | +| `worker.containerSecurityContext.runAsGroup` | Set runAsGroup in worker container' Security Context | `1001` | +| `worker.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in worker container' Security Context | `true` | +| `worker.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in worker container' Security Context | `true` | +| `worker.containerSecurityContext.privileged` | Set privileged in worker container' Security Context | `false` | +| `worker.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in worker container' Security Context | `false` | +| `worker.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in worker container | `["ALL"]` | +| `worker.containerSecurityContext.seccompProfile.type` | Set seccomp profile in worker container | `RuntimeDefault` | +| `worker.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `worker.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `worker.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `worker.serviceAccount.automountServiceAccountToken` | Automount service account token for the worker service account | `true` | +| `worker.serviceAccount.imagePullSecrets` | Add image pull secrets to the worker service account | `[]` | +| `worker.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `worker.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `worker.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `worker.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `worker.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `worker.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | ### app Parameters @@ -509,6 +526,33 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | | `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | | `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | +| `app.exposedContainerPort` | The port that shuffle app containers will listen on for new requests. | `80` | +| `app.podSecurityContext.enabled` | Enable app pods' Security Context | `true` | +| `app.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for app pods | `Always` | +| `app.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for app pods | `[]` | +| `app.podSecurityContext.supplementalGroups` | Set filesystem extra groups for app pods | `[]` | +| `app.podSecurityContext.fsGroup` | Set fsGroup in app pods' Security Context | `1001` | +| `app.containerSecurityContext.enabled` | Enabled app container' Security Context | `true` | +| `app.containerSecurityContext.seLinuxOptions` | Set SELinux options in app container | `{}` | +| `app.containerSecurityContext.runAsUser` | Set runAsUser in app container' Security Context | `1001` | +| `app.containerSecurityContext.runAsGroup` | Set runAsGroup in app container' Security Context | `1001` | +| `app.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in app container' Security Context | `true` | +| `app.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in app container' Security Context | `true` | +| `app.containerSecurityContext.privileged` | Set privileged in app container' Security Context | `false` | +| `app.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in app container' Security Context | `false` | +| `app.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in app container | `["ALL"]` | +| `app.containerSecurityContext.seccompProfile.type` | Set seccomp profile in app container | `RuntimeDefault` | +| `app.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `app.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `app.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `app.serviceAccount.automountServiceAccountToken` | Automount service account token for the app service account | `true` | +| `app.serviceAccount.imagePullSecrets` | Add image pull secrets to the app service account | `[]` | +| `app.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `app.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `app.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | ### Traffic Exposure Parameters @@ -582,6 +626,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 +650,4 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" ### Other Parameters + diff --git a/functions/kubernetes/charts/shuffle/templates/_helpers.tpl b/functions/kubernetes/charts/shuffle/templates/_helpers.tpl index 6bd29238..dc93dfc2 100644 --- a/functions/kubernetes/charts/shuffle/templates/_helpers.tpl +++ b/functions/kubernetes/charts/shuffle/templates/_helpers.tpl @@ -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 -}} {{/* diff --git a/functions/kubernetes/charts/shuffle/templates/backend/backend-cm-env.yaml b/functions/kubernetes/charts/shuffle/templates/backend/backend-cm-env.yaml index e7138dc0..dbbdff94 100644 --- a/functions/kubernetes/charts/shuffle/templates/backend/backend-cm-env.yaml +++ b/functions/kubernetes/charts/shuffle/templates/backend/backend-cm-env.yaml @@ -8,18 +8,20 @@ metadata: annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} data: - BACKEND_PORT: "5001" + BACKEND_PORT: "{{ .Values.backend.containerPorts.http }}" {{- if .Values.shuffle.baseUrl }} BASE_URL: "{{ .Values.shuffle.baseUrl }}" SSO_REDIRECT_URL: "{{ .Values.shuffle.baseUrl }}" {{- else }} - BASE_URL: "http://{{ include "shuffle.backend.name" . }}:5001" + BASE_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.backend.containerPorts.http }}" {{- end }} ORG_ID: "{{ .Values.shuffle.org }}" SHUFFLE_APP_DOWNLOAD_LOCATION: "{{ .Values.backend.apps.downloadLocation }}" SHUFFLE_DOWNLOAD_AUTH_BRANCH: "{{ .Values.backend.apps.downloadBranch }}" SHUFFLE_APP_FORCE_UPDATE: "{{ .Values.backend.apps.forceUpdate }}" SHUFFLE_CHAT_DISABLED: "true" + # Sets backend_url parameter for workflow execution to the cluster-internal shuffle-backend address + SHUFFLE_CLOUDRUN_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.backend.containerPorts.http }}" SHUFFLE_OPENSEARCH_URL: {{ include "common.tplvalues.render" (dict "value" .Values.backend.openSearch.url "context" $) }} SHUFFLE_OPENSEARCH_USERNAME: "{{ .Values.backend.openSearch.username }}" SHUFFLE_OPENSEARCH_CERTIFICATE_FILE: "{{ .Values.backend.openSearch.certificateFile }}" diff --git a/functions/kubernetes/charts/shuffle/templates/backend/backend-svc.yaml b/functions/kubernetes/charts/shuffle/templates/backend/backend-svc.yaml index 18328899..990f9410 100644 --- a/functions/kubernetes/charts/shuffle/templates/backend/backend-svc.yaml +++ b/functions/kubernetes/charts/shuffle/templates/backend/backend-svc.yaml @@ -3,7 +3,8 @@ kind: Service metadata: name: {{ template "shuffle.backend.name" . }} namespace: {{ include "common.names.namespace" . | quote }} - labels: {{- include "shuffle.backend.labels" (dict "customLabels" .Values.commonLabels "context" $) | nindent 4 }} + {{- $serviceLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.service.labels .Values.commonLabels) "context" .) }} + labels: {{- include "shuffle.backend.labels" (dict "customLabels" $serviceLabels "context" $) | nindent 4 }} {{- if .Values.commonAnnotations }} annotations: {{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }} {{- end }} @@ -14,5 +15,6 @@ spec: port: {{ .Values.backend.containerPorts.http }} targetPort: http protocol: TCP + appProtocol: http {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }} selector: {{- include "shuffle.backend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }} diff --git a/functions/kubernetes/charts/shuffle/templates/frontend/frontend-svc.yaml b/functions/kubernetes/charts/shuffle/templates/frontend/frontend-svc.yaml index 76851c0a..de2db9fb 100644 --- a/functions/kubernetes/charts/shuffle/templates/frontend/frontend-svc.yaml +++ b/functions/kubernetes/charts/shuffle/templates/frontend/frontend-svc.yaml @@ -3,7 +3,8 @@ kind: Service metadata: name: {{ template "shuffle.frontend.name" . }} namespace: {{ include "common.names.namespace" . | quote }} - labels: {{- include "shuffle.frontend.labels" (dict "customLabels" .Values.commonLabels "context" $) | nindent 4 }} + {{- $serviceLabels := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.service.labels .Values.commonLabels) "context" .) }} + labels: {{- include "shuffle.frontend.labels" (dict "customLabels" $serviceLabels "context" $) | nindent 4 }} {{- if .Values.commonAnnotations }} annotations: {{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }} {{- end }} @@ -14,11 +15,13 @@ spec: port: {{ .Values.frontend.containerPorts.http }} targetPort: http protocol: TCP + appProtocol: http {{- if .Values.frontend.containerPorts.https }} - name: https port: {{ .Values.frontend.containerPorts.https }} targetPort: https protocol: TCP + appProtocol: https {{- end }} {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.podLabels .Values.commonLabels) "context" .) }} selector: {{- include "shuffle.frontend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }} diff --git a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-cm-env.yaml b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-cm-env.yaml index 57b020a7..504b674d 100644 --- a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-cm-env.yaml +++ b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-cm-env.yaml @@ -11,7 +11,7 @@ data: ENVIRONMENT_NAME: "{{ .Values.shuffle.org }}" ORG_ID: "{{ .Values.shuffle.org }}" TZ: "{{ .Values.shuffle.timezone }}" - BASE_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:5001" + BASE_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.backend.containerPorts.http }}" KUBERNETES_NAMESPACE: "{{ .Release.Namespace }}" KUBERNETES_SERVICE_ACCOUNT: {{ include "shuffle.orborus.serviceAccount.name" . }} SHUFFLE_WORKER_IMAGE: "{{ include "shuffle.worker.image" . }}" diff --git a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-dpl.yaml b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-dpl.yaml index a2d9c278..fd73dd15 100644 --- a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-dpl.yaml +++ b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-dpl.yaml @@ -88,8 +88,26 @@ spec: value: "true" - name: SHUFFLE_WORKER_SERVICE_ACCOUNT_NAME value: {{ include "shuffle.worker.serviceAccount.name" . }} + - name: SHUFFLE_APP_EXPOSED_PORT + value: {{ .Values.app.exposedContainerPort | quote }} + {{- if .Values.worker.podSecurityContext.enabled }} + - name: SHUFFLE_WORKER_POD_SECURITY_CONTEXT + value: {{ omit .Values.worker.podSecurityContext "enabled" | mustToJson | quote }} + {{- end }} + {{- if .Values.worker.containerSecurityContext.enabled }} + - name: SHUFFLE_WORKER_CONTAINER_SECURITY_CONTEXT + value: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.worker.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }} + {{- end }} - name: SHUFFLE_APP_SERVICE_ACCOUNT_NAME value: {{ include "shuffle.app.serviceAccount.name" . }} + {{- if .Values.app.podSecurityContext.enabled }} + - name: SHUFFLE_APP_POD_SECURITY_CONTEXT + value: {{ omit .Values.app.podSecurityContext "enabled" | mustToJson | quote }} + {{- end }} + {{- if .Values.app.containerSecurityContext.enabled }} + - name: SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT + value: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.app.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }} + {{- end }} {{- if .Values.orborus.extraEnvVars }} {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVars "context" $) | nindent 12 }} {{- end }} @@ -110,8 +128,6 @@ spec: resources: {{- include "common.resources.preset" (dict "type" .Values.orborus.resourcesPreset) | nindent 12 }} {{- end }} ports: - - name: http - containerPort: {{ .Values.orborus.containerPorts.http }} {{- if .Values.orborus.extraContainerPorts }} {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraContainerPorts "context" $) | nindent 12 }} {{- end }} @@ -120,25 +136,16 @@ spec: livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customLivenessProbe "context" $) | nindent 12 }} {{- else if .Values.orborus.livenessProbe.enabled }} livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.livenessProbe "enabled") "context" $) | nindent 12 }} - httpGet: - path: / - port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- if .Values.orborus.customReadinessProbe }} readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customReadinessProbe "context" $) | nindent 12 }} {{- else if .Values.orborus.readinessProbe.enabled }} readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.readinessProbe "enabled") "context" $) | nindent 12 }} - httpGet: - path: / - port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- if .Values.orborus.customStartupProbe }} startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customStartupProbe "context" $) | nindent 12 }} {{- else if .Values.orborus.startupProbe.enabled }} startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.startupProbe "enabled") "context" $) | nindent 12 }} - httpGet: - path: / - port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- end }} {{- if .Values.orborus.lifecycleHooks }} diff --git a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-network-policy.yaml b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-network-policy.yaml index f6a22339..f55bf957 100644 --- a/functions/kubernetes/charts/shuffle/templates/orborus/orborus-network-policy.yaml +++ b/functions/kubernetes/charts/shuffle/templates/orborus/orborus-network-policy.yaml @@ -54,18 +54,6 @@ spec: {{- end }} {{- end }} ingress: - - ports: - - port: {{ .Values.orborus.containerPorts.http }} - protocol: TCP - {{- if not .Values.orborus.networkPolicy.allowExternal }} - from: - # Allow traffic from workers - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: {{ .Release.Namespace }} - podSelector: - matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} - {{- end }} {{- if .Values.orborus.networkPolicy.extraIngress }} {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.networkPolicy.extraIngress "context" $ ) | nindent 4 }} {{- end }} diff --git a/functions/kubernetes/charts/shuffle/templates/shuffle-app/shuffle-app-network-policy.yaml b/functions/kubernetes/charts/shuffle/templates/shuffle-app/shuffle-app-network-policy.yaml index d4a24fe6..a8493002 100644 --- a/functions/kubernetes/charts/shuffle/templates/shuffle-app/shuffle-app-network-policy.yaml +++ b/functions/kubernetes/charts/shuffle/templates/shuffle-app/shuffle-app-network-policy.yaml @@ -29,6 +29,16 @@ spec: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system + # Allow access to backend + - ports: + - port: {{ .Values.backend.containerPorts.http }} + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.backend.matchLabels" . | nindent 14 }} # Allow access to workers - ports: - port: 33333 @@ -44,17 +54,18 @@ spec: {{- end }} {{- end }} ingress: - {{- if .Values.app.networkPolicy.allowExternal }} - - {} - {{- else }} - # Allow access from workers. Apps will typicaly use port 80/TCP, but this is not enforced. - - from: + - ports: + - port: {{ .Values.app.exposedContainerPort }} + protocol: TCP + {{- if not .Values.app.networkPolicy.allowExternal }} + # Allow traffic from workers + from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: {{ .Release.Namespace }} podSelector: matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} - {{- end }} + {{- end }} {{- if .Values.app.networkPolicy.extraIngress }} {{- include "common.tplvalues.render" ( dict "value" .Values.app.networkPolicy.extraIngress "context" $ ) | nindent 4 }} {{- end }} diff --git a/functions/kubernetes/charts/shuffle/templates/shuffle-worker/shuffle-worker-network-policy.yaml b/functions/kubernetes/charts/shuffle/templates/shuffle-worker/shuffle-worker-network-policy.yaml index 1b07d9a9..3637d975 100644 --- a/functions/kubernetes/charts/shuffle/templates/shuffle-worker/shuffle-worker-network-policy.yaml +++ b/functions/kubernetes/charts/shuffle/templates/shuffle-worker/shuffle-worker-network-policy.yaml @@ -29,16 +29,6 @@ spec: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system - # Allow access to orborus - - ports: - - port: {{ .Values.orborus.containerPorts.http }} - protocol: TCP - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: {{ .Release.Namespace }} - podSelector: - matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }} # Allow arbitrary connections to apps. Apps will typically use port 80/TCP, but this is not enforced. - to: - namespaceSelector: @@ -62,6 +52,12 @@ spec: kubernetes.io/metadata.name: {{ .Release.Namespace }} podSelector: matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }} + # Allow traffic from apps + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.app.matchLabels" . | nindent 14 }} {{- end }} {{- if .Values.worker.networkPolicy.extraIngress }} {{- include "common.tplvalues.render" ( dict "value" .Values.worker.networkPolicy.extraIngress "context" $ ) | nindent 4 }} diff --git a/functions/kubernetes/charts/shuffle/values.schema.json b/functions/kubernetes/charts/shuffle/values.schema.json index 7a85b9fa..661eefc6 100644 --- a/functions/kubernetes/charts/shuffle/values.schema.json +++ b/functions/kubernetes/charts/shuffle/values.schema.json @@ -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", - "default": "RollingUpdate" + "description": "backend deployment strategy type", + "default": "Recreate" } } }, - "podManagementPolicy": { - "type": "string", - "description": "Pod management policy for backend statefulset", - "default": "OrderedReady" - }, "priorityClassName": { "type": "string", "description": "backend pods' priorityClassName", @@ -693,6 +683,16 @@ } } }, + "service": { + "type": "object", + "properties": { + "labels": { + "type": "object", + "description": "Extra labels for backend service", + "default": {} + } + } + }, "serviceAccount": { "type": "object", "properties": { @@ -839,6 +839,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 +1061,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 +1138,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 +1205,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", @@ -1382,6 +1372,16 @@ } } }, + "service": { + "type": "object", + "properties": { + "labels": { + "type": "object", + "description": "Extra labels for frontend service", + "default": {} + } + } + }, "serviceAccount": { "type": "object", "properties": { @@ -1463,6 +1463,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)", @@ -1486,16 +1491,6 @@ "description": "Number of orborus replicas to deploy", "default": 1 }, - "containerPorts": { - "type": "object", - "properties": { - "http": { - "type": "number", - "description": "orborus HTTP container port", - "default": 8080 - } - } - }, "extraContainerPorts": { "type": "array", "description": "Optionally specify extra list of additional ports for orborus containers", @@ -1675,12 +1670,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 +1747,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 +1814,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 +2072,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)", @@ -2099,6 +2084,103 @@ } } }, + "podSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable worker pods' Security Context", + "default": true + }, + "fsGroupChangePolicy": { + "type": "string", + "description": "Set filesystem group change policy for worker pods", + "default": "Always" + }, + "sysctls": { + "type": "array", + "description": "Set kernel settings using the sysctl interface for worker pods", + "default": [], + "items": {} + }, + "supplementalGroups": { + "type": "array", + "description": "Set filesystem extra groups for worker pods", + "default": [], + "items": {} + }, + "fsGroup": { + "type": "number", + "description": "Set fsGroup in worker pods' Security Context", + "default": 1001 + } + } + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled worker container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set runAsUser in worker container' Security Context", + "default": 1001 + }, + "runAsGroup": { + "type": "number", + "description": "Set runAsGroup in worker container' Security Context", + "default": 1001 + }, + "runAsNonRoot": { + "type": "boolean", + "description": "Set runAsNonRoot in worker container' Security Context", + "default": true + }, + "readOnlyRootFilesystem": { + "type": "boolean", + "description": "Set readOnlyRootFilesystem in worker container' Security Context", + "default": true + }, + "privileged": { + "type": "boolean", + "description": "Set privileged in worker container' Security Context", + "default": false + }, + "allowPrivilegeEscalation": { + "type": "boolean", + "description": "Set allowPrivilegeEscalation in worker container' Security Context", + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "description": "List of capabilities to be dropped in worker container", + "default": [ + "ALL" + ], + "items": { + "type": "string" + } + } + } + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Set seccomp profile in worker container", + "default": "RuntimeDefault" + } + } + } + } + }, "serviceAccount": { "type": "object", "properties": { @@ -2177,6 +2259,103 @@ "app": { "type": "object", "properties": { + "podSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable app pods' Security Context", + "default": true + }, + "fsGroupChangePolicy": { + "type": "string", + "description": "Set filesystem group change policy for app pods", + "default": "Always" + }, + "sysctls": { + "type": "array", + "description": "Set kernel settings using the sysctl interface for app pods", + "default": [], + "items": {} + }, + "supplementalGroups": { + "type": "array", + "description": "Set filesystem extra groups for app pods", + "default": [], + "items": {} + }, + "fsGroup": { + "type": "number", + "description": "Set fsGroup in app pods' Security Context", + "default": 1001 + } + } + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled app container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set runAsUser in app container' Security Context", + "default": 1001 + }, + "runAsGroup": { + "type": "number", + "description": "Set runAsGroup in app container' Security Context", + "default": 1001 + }, + "runAsNonRoot": { + "type": "boolean", + "description": "Set runAsNonRoot in app container' Security Context", + "default": true + }, + "readOnlyRootFilesystem": { + "type": "boolean", + "description": "Set readOnlyRootFilesystem in app container' Security Context", + "default": true + }, + "privileged": { + "type": "boolean", + "description": "Set privileged in app container' Security Context", + "default": false + }, + "allowPrivilegeEscalation": { + "type": "boolean", + "description": "Set allowPrivilegeEscalation in app container' Security Context", + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "description": "List of capabilities to be dropped in app container", + "default": [ + "ALL" + ], + "items": { + "type": "string" + } + } + } + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Set seccomp profile in app container", + "default": "RuntimeDefault" + } + } + } + } + }, "serviceAccount": { "type": "object", "properties": { @@ -2249,6 +2428,11 @@ "items": {} } } + }, + "exposedContainerPort": { + "type": "number", + "description": "The port that shuffle app containers will listen on for new requests. ", + "default": 80 } } }, @@ -2591,6 +2775,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", diff --git a/functions/kubernetes/charts/shuffle/values.yaml b/functions/kubernetes/charts/shuffle/values.yaml index 35492f94..08763cb6 100644 --- a/functions/kubernetes/charts/shuffle/values.yaml +++ b/functions/kubernetes/charts/shuffle/values.yaml @@ -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/ ## @@ -318,25 +310,14 @@ backend: ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## tolerations: [] - ## 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 + ## Backend uses ReadWriteOnce volumes by default, which is incompatible with RollingUpdate ## - 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 + type: Recreate ## @param backend.priorityClassName backend pods' priorityClassName ## priorityClassName: "" @@ -440,6 +421,14 @@ backend: targetCPU: "" targetMemory: "" + ## Service configuration + ## + service: + ## @param backend.service.labels Extra labels for backend service + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + labels: {} + ## ServiceAccount configuration ## serviceAccount: @@ -565,7 +554,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 +562,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 +672,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 +692,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 +718,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 +768,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: "" @@ -908,6 +878,14 @@ frontend: targetCPU: "" targetMemory: "" + ## Service configuration + ## + service: + ## @param frontend.service.labels Extra labels for frontend service + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + labels: {} + ## ServiceAccount configuration ## serviceAccount: @@ -970,7 +948,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 +956,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' @@ -996,10 +974,6 @@ orborus: ## @param orborus.replicaCount Number of orborus replicas to deploy ## replicaCount: 1 - ## @param orborus.containerPorts.http orborus HTTP container port - ## - containerPorts: - http: 8080 ## @param orborus.extraContainerPorts Optionally specify extra list of additional ports for orborus containers ## e.g: ## extraContainerPorts: @@ -1107,8 +1081,8 @@ orborus: containerSecurityContext: enabled: true seLinuxOptions: {} - runAsUser: 101 - runAsGroup: 101 + runAsUser: 1001 + runAsGroup: 1001 runAsNonRoot: true readOnlyRootFilesystem: true privileged: false @@ -1132,18 +1106,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 +1156,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,15 +1331,57 @@ 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: "" + ## Configure Pods Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param worker.podSecurityContext.enabled Enable worker pods' Security Context + ## @param worker.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy for worker pods + ## @param worker.podSecurityContext.sysctls Set kernel settings using the sysctl interface for worker pods + ## @param worker.podSecurityContext.supplementalGroups Set filesystem extra groups for worker pods + ## @param worker.podSecurityContext.fsGroup Set fsGroup in worker pods' Security Context + ## + podSecurityContext: + enabled: true + 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 worker.containerSecurityContext.enabled Enabled worker container' Security Context + ## @param worker.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in worker container + ## @param worker.containerSecurityContext.runAsUser Set runAsUser in worker container' Security Context + ## @param worker.containerSecurityContext.runAsGroup Set runAsGroup in worker container' Security Context + ## @param worker.containerSecurityContext.runAsNonRoot Set runAsNonRoot in worker container' Security Context + ## @param worker.containerSecurityContext.readOnlyRootFilesystem Set readOnlyRootFilesystem in worker container' Security Context + ## @param worker.containerSecurityContext.privileged Set privileged in worker container' Security Context + ## @param worker.containerSecurityContext.allowPrivilegeEscalation Set allowPrivilegeEscalation in worker container' Security Context + ## @param worker.containerSecurityContext.capabilities.drop List of capabilities to be dropped in worker container + ## @param worker.containerSecurityContext.seccompProfile.type Set seccomp profile in worker container + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 1001 + runAsGroup: 1001 + runAsNonRoot: true + readOnlyRootFilesystem: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: "RuntimeDefault" + ## ServiceAccount configuration ## serviceAccount: @@ -1448,6 +1444,48 @@ worker: ## @section app Parameters ## app: + ## Configure Pods Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param app.podSecurityContext.enabled Enable app pods' Security Context + ## @param app.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy for app pods + ## @param app.podSecurityContext.sysctls Set kernel settings using the sysctl interface for app pods + ## @param app.podSecurityContext.supplementalGroups Set filesystem extra groups for app pods + ## @param app.podSecurityContext.fsGroup Set fsGroup in app pods' Security Context + ## + podSecurityContext: + enabled: true + 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 app.containerSecurityContext.enabled Enabled app container' Security Context + ## @param app.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in app container + ## @param app.containerSecurityContext.runAsUser Set runAsUser in app container' Security Context + ## @param app.containerSecurityContext.runAsGroup Set runAsGroup in app container' Security Context + ## @param app.containerSecurityContext.runAsNonRoot Set runAsNonRoot in app container' Security Context + ## @param app.containerSecurityContext.readOnlyRootFilesystem Set readOnlyRootFilesystem in app container' Security Context + ## @param app.containerSecurityContext.privileged Set privileged in app container' Security Context + ## @param app.containerSecurityContext.allowPrivilegeEscalation Set allowPrivilegeEscalation in app container' Security Context + ## @param app.containerSecurityContext.capabilities.drop List of capabilities to be dropped in app container + ## @param app.containerSecurityContext.seccompProfile.type Set seccomp profile in app container + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 1001 + runAsGroup: 1001 + runAsNonRoot: true + readOnlyRootFilesystem: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: "RuntimeDefault" + ## ServiceAccount configuration ## serviceAccount: @@ -1507,6 +1545,10 @@ app: ## extraEgress: [] + ## @param app.exposedContainerPort The port that shuffle app containers will listen on for new requests. + ## + exposedContainerPort: 80 + ## @section Traffic Exposure Parameters ## @@ -1748,7 +1790,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 ## @@ -1802,9 +1844,24 @@ volumePermissions: ## OpenSearch chart configuration ## ref: https://github.com/bitnami/charts/blob/main/bitnami/opensearch/values.yaml ## @param opensearch.enabled Switch to enable or disable the opensearch helm chart +## @skip opensearch.master +## @skip opensearch.data +## @skip opensearch.coordinating +## @skip opensearch.ingest +## @skip opensearch.dashboards ## opensearch: enabled: true + master: + replicaCount: 1 + data: + replicaCount: 1 + coordinating: + replicaCount: 1 + ingest: + replicaCount: 1 + dashboards: + enabled: true ## @section Vault Parameters ## diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile index 023cdf8f..3121e93a 100644 --- a/functions/onprem/orborus/Dockerfile +++ b/functions/onprem/orborus/Dockerfile @@ -1,11 +1,13 @@ -FROM golang:1.22 as builder +FROM golang:1.24 as builder WORKDIR /app COPY orborus.go /app/orborus.go COPY go.mod /app/go.mod -RUN go get +#RUN go get +RUN go mod download +RUN go mod tidy #RUN go build -v # Enabled CGO January 2025 (?) diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 14e73904..5d3ee295 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -1,129 +1,152 @@ module orborus -go 1.22.7 +go 1.24.0 -toolchain go1.22.11 +toolchain go1.24.4 //replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared require ( - github.com/docker/docker v27.5.0+incompatible + github.com/docker/docker v28.2.2+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.0 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 + github.com/shuffle/shuffle-shared v0.8.84 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 ) require ( - cloud.google.com/go v0.110.2 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect - cloud.google.com/go/datastore v1.11.0 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/scheduler v1.9.0 // indirect - cloud.google.com/go/storage v1.29.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect +) + +require ( + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go v0.121.1 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/datastore v1.20.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/scheduler v1.11.7 // indirect + cloud.google.com/go/storage v1.55.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.1.3 // indirect - github.com/adrg/strutil v0.2.3 // indirect - github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/strutil v0.3.1 // indirect + github.com/algolia/algoliasearch-client-go/v3 v3.31.4 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/frikky/kin-openapi v0.41.0 // indirect - github.com/frikky/schemaless v0.0.13 // indirect + github.com/frikky/kin-openapi v0.42.0 // indirect + github.com/frikky/schemaless v0.0.16 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.0 // indirect - github.com/go-git/go-git/v5 v5.13.0 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v28 v28.1.1 // indirect - github.com/google/go-querystring v1.0.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.11.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opensearch-project/opensearch-go v1.1.0 // indirect github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sashabaranov/go-openai v1.19.2 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/sashabaranov/go-openai v1.40.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect - github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect - go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.126.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/api v0.236.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.30.2 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/client-go v0.33.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 98879a0b..13ae08b5 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -7,25 +9,37 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/scheduler v1.9.0 h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM= +cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -33,22 +47,27 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ= -github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1 h1:FP2Xtqqs/sefR5Qluygp+jVV+juXzEdJaPrZTCDLhDQ= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= +github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4 h1:UJhx6AhZCYf0qZygDz2c1x1+1q2q2sfzsRaQM6yswWk= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= @@ -65,105 +84,109 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3w github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= -github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frikky/kin-openapi v0.41.0 h1:oMmjo+ekGS971lb3KLeZZOqRDZOwWi3+g/OiSWP08+s= -github.com/frikky/kin-openapi v0.41.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= -github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ= -github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= +github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0= +github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= +github.com/frikky/schemaless v0.0.16 h1:4d2ZktB9xGsAusbbKliOI8TuriSrdIMzD/6ToY3wkz8= +github.com/frikky/schemaless v0.0.16/go.mod h1:jT48kTcmr1q3o8i+8qe7g+eCsbwaz2Q9CjOJevQQzQs= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-git/v5 v5.16.1 h1:TuxMBWNL7R05tXsUGi0kh1vi4tq0WfXNLlIrAkXG1k8= +github.com/go-git/go-git/v5 v5.16.1/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -172,70 +195,55 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -264,6 +272,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -275,60 +287,61 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= -github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= -github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU= +github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs= +github.com/sendgrid/sendgrid-go v3.16.1+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.0 h1:8ABtFz5UYOXN6mnnu/LrOwz8TxASnu1OBbj+gfqmtYk= -github.com/shuffle/shuffle-shared v0.8.0/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= +github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM= +github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk= 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= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -338,54 +351,59 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= -go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= -go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -413,9 +431,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -429,33 +444,23 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -464,9 +469,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -480,8 +484,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -492,24 +494,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -520,13 +513,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -554,15 +546,12 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -571,8 +560,8 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0= +google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -593,85 +582,72 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= -k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= -k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 2f6af9fe..e350082b 100755 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -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,17 @@ 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 workerPodSecurityContext = os.Getenv("SHUFFLE_WORKER_POD_SECURITY_CONTEXT") +var workerContainerSecurityContext = os.Getenv("SHUFFLE_WORKER_CONTAINER_SECURITY_CONTEXT") +var appServiceAccountName = os.Getenv("SHUFFLE_APP_SERVICE_ACCOUNT_NAME") +var appPodSecurityContext = os.Getenv("SHUFFLE_APP_POD_SECURITY_CONTEXT") +var appContainerSecurityContext = os.Getenv("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT") // var baseimagename = "docker.pkg.github.com/shuffle/shuffle" // var baseimagename = "ghcr.io/frikky" @@ -120,6 +123,7 @@ var tenzirDisabled = false var dockercli *dockerclient.Client var containerId string var executionCount = 0 +var orborusUuid = os.Getenv("SHUFFLE_ORBORUS_UUID") var imagedownloadTimeout = time.Second * 300 var window = shuffle.NewTimeWindow(1 * time.Minute) @@ -217,8 +221,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 +233,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) @@ -394,7 +373,7 @@ func deployServiceWorkers(image string) { options := make(map[string]string) options["com.docker.network.driver.mtu"] = fmt.Sprintf("%d", mtu) - ingressOptions := types.NetworkCreate{ + ingressOptions := network.CreateOptions{ Driver: "overlay", Attachable: false, Ingress: true, @@ -426,7 +405,7 @@ func deployServiceWorkers(image string) { networkName = swarmNetworkName } - networkCreateOptions := types.NetworkCreate{ + networkCreateOptions := network.CreateOptions{ Driver: "overlay", Options: options, Attachable: true, @@ -456,18 +435,21 @@ 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" - - 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 if containerId != "" { @@ -487,9 +469,11 @@ func deployServiceWorkers(image string) { if len(os.Getenv("DOCKER_HOST")) > 0 { log.Printf("[DEBUG] Deploying docker socket proxy to the network %s as the DOCKER_HOST variable is set", networkName) - containers, err := dockercli.ContainerList(ctx, container.ListOptions{ + + listOptions := container.ListOptions{ All: true, - }) + } + containers, err := dockercli.ContainerList(ctx, listOptions) if err == nil { for _, container := range containers { @@ -608,7 +592,7 @@ func deployServiceWorkers(image string) { fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", os.Getenv("SHUFFLE_LOGS_DISABLED")), fmt.Sprintf("DEBUG_MEMORY=%s", os.Getenv("DEBUG_MEMORY")), fmt.Sprintf("SHUFFLE_APP_SDK_TIMEOUT=%s", os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")), - fmt.Sprintf("SHUFFLE_MAX_SWARM_NODES=%d", os.Getenv("SHUFFLE_MAX_SWARM_NODES")), + fmt.Sprintf("SHUFFLE_MAX_SWARM_NODES=%s", os.Getenv("SHUFFLE_MAX_SWARM_NODES")), fmt.Sprintf("SHUFFLE_BASE_IMAGE_NAME=%s", os.Getenv("SHUFFLE_BASE_IMAGE_NAME")), fmt.Sprintf("SHUFFLE_APP_REQUEST_TIMEOUT=%s", os.Getenv("SHUFFLE_APP_REQUEST_TIMEOUT")), }, @@ -627,13 +611,31 @@ func deployServiceWorkers(image string) { if defaultNetworkAttach == true || strings.ToLower(os.Getenv("SHUFFLE_DEFAULT_NETWORK_ATTACH")) == "true" { targetName := "shuffle_shuffle" - log.Printf("[DEBUG] Adding network attach for network %s to worker in swarm", targetName) - serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{ - Target: targetName, - }) + isAttachable := false + networks, err := dockercli.NetworkList(ctx, network.ListOptions{}) + if err == nil { + for _, net := range networks { + if net.Name == targetName { + if net.Scope == "swarm" { + log.Printf("[DEBUG] Found swarm-scoped network: %s", targetName) + isAttachable = true + } else { + log.Printf("[WARNING] Network %s exist but is not swarm scoped (scope=%s)", targetName, net.Scope) + } + break + } + } + } - // FIXM: Remove this if deployment fails? - serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=%s", targetName)) + if isAttachable { + log.Printf("[DEBUG] Adding network attach for network %s to worker in swarm", targetName) + serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{ + Target: targetName, + }) + + // FIXM: Remove this if deployment fails? + serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=%s", targetName)) + } } if dockerApiVersion != "" { @@ -722,11 +724,38 @@ 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") { log.Printf("[ERROR] Failed making service: %s", err) + if strings.Contains(fmt.Sprintf("%s", err), "networks scoped to the swarm can be used") { + log.Printf("[WARNING] Swarm network attachment failed, retrying without shuffle_shuffle") + + var updatedNetworks []swarm.NetworkAttachmentConfig + for _, net := range serviceSpec.Networks { + if net.Target != "shuffle_shuffle" { + updatedNetworks = append(updatedNetworks, net) + } + } + serviceSpec.Networks = updatedNetworks + + var updatedEnv []string + for _, env := range serviceSpec.TaskTemplate.ContainerSpec.Env { + if !strings.HasPrefix(env, "SHUFFLE_SWARM_OTHER_NETWORK=") { + updatedEnv = append(updatedEnv, env) + } + } + serviceSpec.TaskTemplate.ContainerSpec.Env = updatedEnv + serviceOptions := types.ServiceCreateOptions{} + _, err = dockercli.ServiceCreate( + ctx, + serviceSpec, + serviceOptions, + ) + if err != nil { + log.Printf("[ERROR] Failed to deploy service even without shuffle_shuffle network: %s", err) + } + } } else { log.Printf("[WARNING] Failed deploying workers: %s", err) if len(serviceSpec.Networks) > 1 { @@ -766,45 +795,54 @@ func handleBackendImageDownload(ctx context.Context, images string) error { // Remove the image handled := []string{} - log.Printf("[DEBUG] Should remove existing image (s): %s. Waiting 30 seconds to ensure backend has the latest images built and ready to distribute.", images) - removeOptions := image.RemoveOptions{} - - time.Sleep(time.Duration(30) * time.Second) - + //log.Printf("[DEBUG] Removing existing image (s): %s", images) newImages := []string{} - for _, image := range strings.Split(images, ",") { - image = strings.TrimSpace(image) - if shuffle.ArrayContains(handled, image) { + + successful := []string{} + for _, curimage := range strings.Split(images, ",") { + curimage = strings.TrimSpace(curimage) + if shuffle.ArrayContains(handled, curimage) { continue } - handled = append(handled, image) - if !strings.Contains(image, "/") { - image = fmt.Sprintf("frikky/shuffle:%s", image) + handled = append(handled, curimage) + if !strings.Contains(curimage, "/") { + curimage = fmt.Sprintf("frikky/shuffle:%s", curimage) } - newImages = append(newImages, image) + newImages = append(newImages, curimage) - // There is no real point in actual removal. This may however be a good idea, as Worker will force download the new one anyway - resp, err := dockercli.ImageRemove(ctx, image, removeOptions) + // Force remove the current image to avoid cached layers +// 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) +// } else { +// log.Printf("[DEBUG] Removed image: %s", curimage) +// } +// } else { +// //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) if err != nil { - log.Printf("[ERROR] Failed removing image: %s. Resp: %#v", err, resp) - - // Goroutining images that don't already exist, as they are most likely not the correct one - go shuffle.DownloadDockerImageBackend(&http.Client{Timeout: imagedownloadTimeout}, image) + //log.Printf("[ERROR] Failed downloading image: %s", err) } else { - log.Printf("[DEBUG] Removed image: %s", image) - - err = shuffle.DownloadDockerImageBackend(&http.Client{Timeout: imagedownloadTimeout}, image) - if err != nil { - log.Printf("[ERROR] Failed downloading image: %s", err) - } else { - log.Printf("[DEBUG] Downloaded image: %s", image) - //break - } + //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")) @@ -820,24 +858,23 @@ func handleBackendImageDownload(ctx context.Context, images string) error { ) if err != nil { - log.Printf("[ERROR] Failed finding containers: %s", err) + log.Printf("[ERROR] Failed finding services: %s", err) } else { - log.Printf("[DEBUG] Found %d services", len(services)) - + found := false for _, service := range services { - - log.Printf("Imagename: %s", service.Spec.TaskTemplate.ContainerSpec.Image) + //log.Printf("Service image: %s", service.Spec.TaskTemplate.ContainerSpec.Image) for _, image := range newImages { if !strings.Contains(service.Spec.TaskTemplate.ContainerSpec.Image, image) { continue } - log.Printf("[DEBUG] Found service for image %#v: %#v", service.Spec.Annotations.Name) + log.Printf("[DEBUG] Found service for image: %#v", service.Spec.Annotations.Name) // Update the service to run with the new image //docker service update --image username/imagename:latest servicename --force serviceUpdateOptions := types.ServiceUpdateOptions{} + service.Spec.TaskTemplate.ForceUpdate++ resp, err := dockercli.ServiceUpdate( ctx, service.ID, @@ -850,12 +887,24 @@ func handleBackendImageDownload(ctx context.Context, images string) error { log.Printf("[ERROR] Failed updating service %s with the new image %s: %s. Resp: %#v", service.Spec.Annotations.Name, image, err, resp) } else { log.Printf("[DEBUG] Updated service %s with the new image %s. Resp: %#v", service.Spec.Annotations.Name, image, resp) + + found = true if !strings.Contains(fmt.Sprintf("%s", resp), "error") { break + } else { + log.Printf("[ERROR] Failed updating service %s with the new image %s: %s. Resp: %#v", service.Spec.Annotations.Name, image, err, resp) } } } + + if found { + break + } + } + + if !found { + log.Printf("[DEBUG] Failed to find service to update for service %s", newImages) } } @@ -976,8 +1025,6 @@ func fixk8sRoles() { } } -func int32Ptr(i int32) *int32 { return &i } - func deployK8sWorker(image string, identifier string, env []string) error { env = append(env, fmt.Sprintf("IS_KUBERNETES=true")) env = append(env, fmt.Sprintf("KUBERNETES_NAMESPACE=%s", os.Getenv("KUBERNETES_NAMESPACE"))) @@ -1002,6 +1049,22 @@ 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(os.Getenv("SHUFFLE_APP_EXPOSED_PORT")) > 0 { + env = append(env, fmt.Sprintf("SHUFFLE_APP_EXPOSED_PORT=%s", os.Getenv("SHUFFLE_APP_EXPOSED_PORT"))) + } + + if len(appServiceAccountName) > 0 { + env = append(env, fmt.Sprintf("SHUFFLE_APP_SERVICE_ACCOUNT_NAME=%s", appServiceAccountName)) + } + + if len(appPodSecurityContext) > 0 { + env = append(env, fmt.Sprintf("SHUFFLE_APP_POD_SECURITY_CONTEXT=%s", appPodSecurityContext)) + } + + if len(appContainerSecurityContext) > 0 { + env = append(env, fmt.Sprintf("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT=%s", appContainerSecurityContext)) + } + clientset, _, err := shuffle.GetKubernetesClient() if err != nil { log.Printf("[ERROR] Error getting kubernetes client:", err) @@ -1010,18 +1073,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") @@ -1080,14 +1131,48 @@ func deployK8sWorker(image string, identifier string, env []string) error { } } - containerLabels := map[string]string{ + labels := map[string]string{ + // Well-known Kubernetes labels + "app.kubernetes.io/name": "shuffle-worker", + "app.kubernetes.io/instance": identifier, + "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, + } + + // Parse security contexts from env + var podSecurityContext *corev1.PodSecurityContext + var containerSecurityContext *corev1.SecurityContext + + if len(workerPodSecurityContext) > 0 { + podSecurityContext = &corev1.PodSecurityContext{} + err = json.Unmarshal([]byte(workerPodSecurityContext), podSecurityContext) + if err != nil { + log.Printf("[ERROR] Failed to unmarshal worker pod security context: %v", err) + return fmt.Errorf("failed to unmarshal worker pod security context: %v", err) + } + } + + if len(workerContainerSecurityContext) > 0 { + containerSecurityContext = &corev1.SecurityContext{} + err = json.Unmarshal([]byte(workerContainerSecurityContext), containerSecurityContext) + if err != nil { + log.Printf("[ERROR] Failed to unmarshal worker container security context: %v", err) + return fmt.Errorf("failed to unmarshal worker container security context: %v", err) + } + } + containerAttachment := corev1.Container{ - Name: identifier, - Image: kubernetesImage, - Env: buildEnvVars(envMap), + Name: identifier, + Image: kubernetesImage, + Env: buildEnvVars(envMap), + SecurityContext: containerSecurityContext, //ImagePullPolicy: "Never", ImagePullPolicy: corev1.PullIfNotPresent, @@ -1186,22 +1271,25 @@ 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: int32Ptr(replicaNumberInt32), + 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, + SecurityContext: podSecurityContext, }, }, }, @@ -1213,13 +1301,13 @@ func deployK8sWorker(image string, identifier string, env []string) error { return err } - // 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", @@ -1227,7 +1315,7 @@ func deployK8sWorker(image string, identifier string, env []string) error { TargetPort: intstr.FromInt(33333), }, }, - Type: corev1.ServiceTypeNodePort, + Type: corev1.ServiceTypeClusterIP, }, } @@ -1242,7 +1330,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"))) } @@ -1272,18 +1359,16 @@ 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, @@ -1302,7 +1387,6 @@ func deployWorker(image string, identifier string, env []string, executionReques } } - //var swarmConfig = os.Getenv("SHUFFLE_SWARM_CONFIG") parsedUuid := uuid.NewV4() @@ -1314,7 +1398,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 } } @@ -1343,11 +1427,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 } } @@ -1372,47 +1456,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) @@ -1460,47 +1544,74 @@ 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) + } + + // Old sane default overrides: + if baseimageregistry == "ghcr.io" && baseimagename == "shuffle" { + baseimageregistry = "docker.io" + baseimagename = "frikky/shuffle" + + os.Setenv("REGISTRY_URL", baseimageregistry) + os.Setenv("SHUFFLE_BASE_IMAGE_REGISTRY", baseimageregistry) + os.Setenv("SHUFFLE_BASE_IMAGE_NAME", baseimagename) + + log.Printf("[WARNING] Overriding bad defaults of ghcr.io/shuffle") } 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 - 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, - } + // 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"), + newWorker, + } - pullOptions := image.PullOptions{} - for _, image := range images { - if isKubernetes == "true" { - log.Printf("[DEBUG] Skipping image pull of '%s' because Kubernetes does it in realtime instead", image) - } else { - log.Printf("[DEBUG] Pulling image %s", image) - reader, err := dockercli.ImagePull(ctx, image, pullOptions) - if err != nil { - log.Printf("[ERROR] Failed getting image %s: %s", image, err) + pullOptions := image.PullOptions{} + for _, image := range images { + if isKubernetes == "true" { + log.Printf("[DEBUG] Skipping image pull of '%s' because Kubernetes does it in realtime instead", image) + } else { + log.Printf("[DEBUG] Pulling image %s", image) + reader, err := dockercli.ImagePull(ctx, image, pullOptions) + if err != nil { + log.Printf("[ERROR] Failed getting image %s: %s", image, err) - continue + continue + } + + io.Copy(os.Stdout, reader) + log.Printf("[DEBUG] Successfully downloaded and built %s", image) } - - io.Copy(os.Stdout, reader) - log.Printf("[DEBUG] Successfully downloaded and built %s", image) } } } @@ -1602,7 +1713,8 @@ func getContainerResourceUsage(ctx context.Context, cli *dockerclient.Client, co } func parseResourceUsage(body io.Reader) (float64, float64, error) { - var stats types.StatsJSON + //var stats types.StatsJSON + var stats container.Stats // Decode the stream of stats as JSON decoder := json.NewDecoder(body) @@ -1637,6 +1749,8 @@ func getOrborusStats(ctx context.Context) shuffle.OrborusStats { Environment: environment, OrborusLabel: orborusLabel, Timestamp: time.Now().Unix(), + + Uuid: orborusUuid, } if (swarmConfig == "run" || swarmConfig == "swarm") && strings.Contains(newWorkerImage, "scale") { @@ -1893,6 +2007,10 @@ func main() { //baseUrl = "http://localhost:5001" } + if len(orborusUuid) == 0 { + orborusUuid = uuid.NewV4().String() + } + //if orgId == "" { // log.Printf("[ERROR] Org not defined. Set variable ORG_ID based on your org") // os.Exit(3) @@ -1918,7 +2036,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" } @@ -1995,7 +2113,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 } @@ -2095,6 +2213,8 @@ func main() { } log.Printf("[INFO] Waiting for executions at %s with Environment %#v", fullUrl, environment) + + hasStarted := false for { if req.Method == "POST" { @@ -2106,6 +2226,7 @@ func main() { // Marshal and set body orborusStats := getOrborusStats(ctx) + pipelinePayload, pipelineerr := sendPipelineHealthStatus() if pipelineerr != nil { @@ -2164,8 +2285,12 @@ func main() { continue } - // FIXME - add check for StatusCode - if newresp.StatusCode != 200 { + // Controls Leader/Follower mode + if newresp.StatusCode == 409 { + log.Printf("[INFO] Another Orborus is already handling jobs. Polling every 30 seconds in case Leader stops. Resp: %s", string(body)) + time.Sleep(time.Duration(30) * time.Second) + continue + } else if newresp.StatusCode != 200 { log.Printf("[ERROR] Backend connection failed for url '%s', or is missing (%d): %s", fullUrl, newresp.StatusCode, string(body)) } else { if !hasStarted { @@ -2235,7 +2360,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) @@ -2245,12 +2370,9 @@ func main() { toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } else if incRequest.Type == "DOCKER_IMAGE_DOWNLOAD" { - log.Printf("[INFO] Should delete -> download new images: %#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.") @@ -2260,7 +2382,7 @@ func main() { } else if incRequest.Type == "CATEGORY_UPDATE" { os.Setenv("SHUFFLE_SKIP_PIPELINES", "false") - tenzirDisabled = false + tenzirDisabled = false err = handleFileCategoryChange() if err != nil { @@ -2305,7 +2427,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) @@ -2315,7 +2437,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( @@ -2413,11 +2535,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? @@ -2477,7 +2599,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"))) } @@ -2507,10 +2629,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) + } + } } } } @@ -2634,7 +2770,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" { @@ -2834,12 +2970,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) @@ -2895,7 +3031,8 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri } func createNetworkIfNotExists(ctx context.Context, networkName, subnet, gateway string) error { - networks, err := dockercli.NetworkList(ctx, types.NetworkListOptions{}) + listOptions := network.ListOptions{} + networks, err := dockercli.NetworkList(ctx, listOptions) if err != nil { return err } @@ -2916,7 +3053,7 @@ func createNetworkIfNotExists(ctx context.Context, networkName, subnet, gateway }, } - networkCreate := types.NetworkCreate{ + networkCreate := network.CreateOptions{ //CheckDuplicate: true, Driver: "bridge", IPAM: ipamConfig, @@ -3047,7 +3184,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) @@ -3065,8 +3201,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"` } @@ -3202,10 +3338,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 } @@ -3289,7 +3443,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 { @@ -3297,7 +3451,6 @@ func handleFileCategoryChange() error { return err } - log.Printf("[DEBUG] Detection files copied to '%s' successfully.", sigmaPath) return nil @@ -3323,6 +3476,11 @@ func extractZIP(zipFile, destDir string) error { } for _, f := range r.File { + // Fix path traversal + if strings.Contains(f.Name, "..") { + return fmt.Errorf("illegal file name: %s", f.Name) + } + err := extractFile(f, destDir) if err != nil { return err @@ -3381,7 +3539,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) @@ -3428,6 +3586,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 { @@ -3450,10 +3612,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") { @@ -3462,7 +3620,7 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) { } else { tenzirDisabled = true - log.Printf("[ERROR] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err) + log.Printf("[WARNING] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err) } return pipelinePayload, err @@ -3546,9 +3704,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) @@ -3637,15 +3794,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)) @@ -3794,8 +3948,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 } @@ -3819,8 +3972,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 } diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index 019e0fc7..e25112d0 100755 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22 as builder +FROM golang:1.24 as builder WORKDIR /app #RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client @@ -9,7 +9,8 @@ COPY go.mod /app/go.mod #COPY go.sum /app/go.sum #RUN go #COPY go.sum /app/go.sum -RUN go get +RUN go mod download +RUN go mod tidy #RUN go mod init worker #RUN go get diff --git a/functions/onprem/worker/go.mod b/functions/onprem/worker/go.mod index d1579270..1df6cb59 100644 --- a/functions/onprem/worker/go.mod +++ b/functions/onprem/worker/go.mod @@ -1,130 +1,153 @@ module worker -go 1.22.7 +go 1.24.0 -toolchain go1.22.11 +toolchain go1.24.4 + +//replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared require ( - github.com/docker/docker v27.5.0+incompatible + github.com/docker/docker v28.2.2+incompatible github.com/gorilla/mux v1.8.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.2 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 - k8s.io/client-go v0.30.2 + github.com/shuffle/shuffle-shared v0.8.84 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 ) require ( - cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute v1.24.0 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect - cloud.google.com/go/datastore v1.15.0 // indirect - cloud.google.com/go/iam v1.1.6 // indirect - cloud.google.com/go/scheduler v1.10.6 // indirect - cloud.google.com/go/storage v1.36.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect +) + +require ( + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go v0.121.1 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/datastore v1.20.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/scheduler v1.11.7 // indirect + cloud.google.com/go/storage v1.55.0 // indirect dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.1.3 // indirect - github.com/adrg/strutil v0.2.3 // indirect - github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/strutil v0.3.1 // indirect + github.com/algolia/algoliasearch-client-go/v3 v3.31.4 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/cyphar/filepath-securejoin v0.2.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/frikky/kin-openapi v0.41.0 // indirect - github.com/frikky/schemaless v0.0.13 // indirect + github.com/frikky/kin-openapi v0.42.0 // indirect + github.com/frikky/schemaless v0.0.16 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.0 // indirect - github.com/go-git/go-git/v5 v5.13.0 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v28 v28.1.1 // indirect - github.com/google/go-querystring v1.0.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opensearch-project/opensearch-go v1.1.0 // indirect github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sashabaranov/go-openai v1.19.2 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/sashabaranov/go-openai v1.40.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect - github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/api v0.162.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/api v0.236.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/functions/onprem/worker/go.sum b/functions/onprem/worker/go.sum index fd10cb6c..0ef2d3a6 100644 --- a/functions/onprem/worker/go.sum +++ b/functions/onprem/worker/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -7,49 +9,63 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= -cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= -cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= -cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/scheduler v1.10.6 h1:5U8iXLoQ03qOB+ZXlAecU7fiE33+u3QiM9nh4cd0eTE= -cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE= +cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM= +cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8= -cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ= -github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1 h1:FP2Xtqqs/sefR5Qluygp+jVV+juXzEdJaPrZTCDLhDQ= -github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= +github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4 h1:UJhx6AhZCYf0qZygDz2c1x1+1q2q2sfzsRaQM6yswWk= +github.com/algolia/algoliasearch-client-go/v3 v3.31.4/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -68,105 +84,109 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3w github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= -github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= -github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= -github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frikky/kin-openapi v0.41.0 h1:oMmjo+ekGS971lb3KLeZZOqRDZOwWi3+g/OiSWP08+s= -github.com/frikky/kin-openapi v0.41.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= -github.com/frikky/schemaless v0.0.13 h1:ARiN9V7wr2VZXAr9JK5wvTbyPgpGrgeiL1VhR5MlgaQ= -github.com/frikky/schemaless v0.0.13/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= +github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0= +github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= +github.com/frikky/schemaless v0.0.16 h1:4d2ZktB9xGsAusbbKliOI8TuriSrdIMzD/6ToY3wkz8= +github.com/frikky/schemaless v0.0.16/go.mod h1:jT48kTcmr1q3o8i+8qe7g+eCsbwaz2Q9CjOJevQQzQs= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-git/v5 v5.16.1 h1:TuxMBWNL7R05tXsUGi0kh1vi4tq0WfXNLlIrAkXG1k8= +github.com/go-git/go-git/v5 v5.16.1/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -175,69 +195,57 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -266,8 +274,11 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -278,69 +289,61 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= -github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= -github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU= +github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= -github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs= +github.com/sendgrid/sendgrid-go v3.16.1+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.6.74 h1:os3BDSFZnl4U8ZgsTAY8IsTDADcMXhbc1rS9UMa0BIY= -github.com/shuffle/shuffle-shared v0.6.74/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus= -github.com/shuffle/shuffle-shared v0.6.90 h1:FzIYtEt44eWgEsW/9tj2ki7qq8FEm/HWXUok+THp72M= -github.com/shuffle/shuffle-shared v0.6.90/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus= -github.com/shuffle/shuffle-shared v0.7.0 h1:T7nbC5An/K9ePx1pwvYqSnnNZuafwK3GVy6wPDuog18= -github.com/shuffle/shuffle-shared v0.7.0/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= -github.com/shuffle/shuffle-shared v0.7.82 h1:La11F5jp9bNtM3VuR9PawyWo90/vZ+1Txo4NIc4YdPg= -github.com/shuffle/shuffle-shared v0.7.82/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= -github.com/shuffle/shuffle-shared v0.7.83 h1:OyyDo0ii8rOYHN5wGbcM94JuDKLmbZ9jhMQ0+/KMb0A= -github.com/shuffle/shuffle-shared v0.7.83/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= -github.com/shuffle/shuffle-shared v0.8.0 h1:8ABtFz5UYOXN6mnnu/LrOwz8TxASnu1OBbj+gfqmtYk= -github.com/shuffle/shuffle-shared v0.8.0/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= +github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM= +github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk= 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= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -350,51 +353,50 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= -go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= -go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= -go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= -go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= -go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= -go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -402,12 +404,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -435,11 +433,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -453,32 +446,23 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -487,11 +471,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -505,34 +486,25 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -543,15 +515,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -579,17 +548,12 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -598,8 +562,8 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps= -google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0= +google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0= +google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -620,88 +584,72 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= -google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= -k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= -k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 7595572b..2fbf51e3 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -23,8 +23,9 @@ 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" + "github.com/docker/docker/api/types/network" dockerclient "github.com/docker/docker/client" // This is for automatic removal of certain code :) @@ -57,11 +58,15 @@ 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 baseimagename = "frikky/shuffle" +// Kubernetes settings +var appServiceAccountName = os.Getenv("SHUFFLE_APP_SERVICE_ACCOUNT_NAME") +var appPodSecurityContext = os.Getenv("SHUFFLE_APP_POD_SECURITY_CONTEXT") +var appContainerSecurityContext = os.Getenv("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT") var kubernetesNamespace = os.Getenv("KUBERNETES_NAMESPACE") + var executionCount int64 -// var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME") +var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME") // var baseimagename = "registry.hub.docker.com/frikky/shuffle" var registryName = "registry.hub.docker.com" @@ -73,6 +78,7 @@ var appsInitialized = false var hostname string var maxReplicas = uint64(12) +var debug bool /* var environments []string @@ -105,11 +111,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", - "http:1.3.0": "frikky/shuffle:http_1.3.0", - "shuffle-tools:1.2.0": "frikky/shuffle:shuffle-tools_1.2.0", - "shuffle-subflow:1.0.0": "frikky/shuffle:shuffle-subflow_1.0.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", } @@ -158,7 +162,7 @@ func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.Workflo log.Printf("[ERROR] Failed marshalling shutdowndata during set: %s", err) } - log.Printf("[DEBUG][%s] Sending result (set)", workflowExecution.ExecutionId) + log.Printf("[DEBUG][%s] Sending result (set). Status: %s, Actions: %d, Results: %d", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Workflow.Actions), len(workflowExecution.Results)) sendResult(workflowExecution, shutdownData) return nil } @@ -301,7 +305,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() @@ -392,8 +396,6 @@ func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason } } -func int32Ptr(i int32) *int32 { return &i } - // ** STARTREMOVE ***/ func deployk8sApp(image string, identifier string, env []string) error { if len(os.Getenv("KUBERNETES_NAMESPACE")) > 0 { @@ -402,6 +404,11 @@ func deployk8sApp(image string, identifier string, env []string) error { kubernetesNamespace = "default" } + deployport, err := strconv.Atoi(os.Getenv("SHUFFLE_APP_EXPOSED_PORT")) + if err != nil { + deployport = 80 + } + envMap := make(map[string]string) for _, envStr := range env { parts := strings.SplitN(envStr, "=", 2) @@ -411,9 +418,7 @@ func deployk8sApp(image string, identifier string, env []string) error { } // add to env - // fmt.Sprintf("SHUFFLE_APP_EXPOSED_PORT=%d", deployport), - // fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", os.Getenv("SHUFFLE_SWARM_CONFIG")), - envMap["SHUFFLE_APP_EXPOSED_PORT"] = "80" + envMap["SHUFFLE_APP_EXPOSED_PORT"] = strconv.Itoa(deployport) envMap["SHUFFLE_SWARM_CONFIG"] = os.Getenv("SHUFFLE_SWARM_CONFIG") envMap["BASE_URL"] = "http://shuffle-workers:33333" @@ -487,9 +492,49 @@ 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{ + // Well-known Kubernetes labels + "app.kubernetes.io/name": "shuffle-app", + "app.kubernetes.io/instance": name, + "app.kubernetes.io/part-of": "shuffle", + "app.kubernetes.io/managed-by": "shuffle-worker", + // Keep legacy labels for backward compatibility + "app": name, + // TODO: Add Shuffle specific labels + // "app.shuffler.io/name": "APP_NAME", + // "app.shuffler.io/version": "APP_VERSION", + } + + matchLabels := map[string]string{ + "app.kubernetes.io/name": "shuffle-app", + "app.kubernetes.io/instance": name, + } + + // Parse security contexts from env + var podSecurityContext *corev1.PodSecurityContext + var containerSecurityContext *corev1.SecurityContext + + if len(appPodSecurityContext) > 0 { + podSecurityContext = &corev1.PodSecurityContext{} + err = json.Unmarshal([]byte(appPodSecurityContext), podSecurityContext) + if err != nil { + log.Printf("[ERROR] Failed to unmarshal app pod security context: %v", err) + return fmt.Errorf("failed to unmarshal app pod security context: %v", err) + } + } + + if len(appContainerSecurityContext) > 0 { + containerSecurityContext = &corev1.SecurityContext{} + err = json.Unmarshal([]byte(appContainerSecurityContext), containerSecurityContext) + if err != nil { + log.Printf("[ERROR] Failed to unmarshal app container security context: %v", err) + return fmt.Errorf("failed to unmarshal app container security context: %v", err) + } + } // pod := &corev1.Pod{ // ObjectMeta: metav1.ObjectMeta{ @@ -569,20 +614,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: int32Ptr(replicaNumberInt32), + 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{ @@ -590,8 +632,18 @@ func deployk8sApp(image string, identifier string, env []string) error { Name: value, Image: image, Env: buildEnvVars(envMap), + Ports: []corev1.ContainerPort{ + { + Protocol: "TCP", + ContainerPort: int32(deployport), + }, + }, + SecurityContext: containerSecurityContext, }, }, + DNSPolicy: corev1.DNSClusterFirst, + ServiceAccountName: appServiceAccountName, + SecurityContext: podSecurityContext, }, }, }, @@ -603,23 +655,21 @@ func deployk8sApp(image string, identifier string, env []string) error { return err } - // 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", Port: 80, - TargetPort: intstr.FromInt(80), + TargetPort: intstr.FromInt(deployport), }, }, - Type: corev1.ServiceTypeNodePort, + Type: corev1.ServiceTypeClusterIP, }, } @@ -875,7 +925,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 } @@ -885,19 +935,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, "~") { @@ -905,15 +966,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{ - Type: mount.TypeBind, - Source: sourceFolder, - Target: destinationFolder, - }) + + 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) } } @@ -922,6 +995,8 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] Env: env, } + //log.Printf("[DEBUG] Deploying image with env: %#v", env) + // Checking as late as possible, just in case. newExecId := fmt.Sprintf("%s_%s", workflowExecution.ExecutionId, action.ID) _, err := shuffle.GetCache(ctx, newExecId) @@ -957,7 +1032,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, @@ -1038,17 +1114,21 @@ 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 if possible + go cli.ContainerRemove(ctx, cont.ID, container.RemoveOptions{}) + + 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(), @@ -1070,12 +1150,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 { @@ -1235,6 +1315,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 } @@ -1477,10 +1558,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 @@ -1504,9 +1581,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") { @@ -1792,6 +1871,8 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { } func executionInit(workflowExecution shuffle.WorkflowExecution) error { + ctx := context.Background() + parents := map[string][]string{} children := map[string][]string{} nextActions := []string{} @@ -1812,6 +1893,20 @@ func executionInit(workflowExecution shuffle.WorkflowExecution) error { } } + // Validates RERUN of single actions + // Identified by: + // 1. Predefined result from previous exec + // 2. Only ONE action + // 3. Every predefined result having result.Action.Category == "rerun" + /* + if len(workflowExecution.Workflow.Actions) == 1 && len(workflowExecution.Results) > 0 { + finished := shuffle.ValidateFinished(ctx, extra, workflowExecution) + if finished { + return nil + } + } + */ + nextActions = append(nextActions, startAction) for _, branch := range workflowExecution.Workflow.Branches { // Check what the parent is first. If it's trigger - skip @@ -1899,8 +1994,6 @@ func executionInit(workflowExecution shuffle.WorkflowExecution) error { //log.Printf("Successfully downloaded and built %s", image) } - ctx := context.Background() - visited := []string{} executed := []string{} environments := []string{} @@ -2282,6 +2375,54 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar { } return envVars } +func getWorkerBackendExecution(auth string, executionId string) (*shuffle.WorkflowExecution, error) { + backendUrl := os.Getenv("BASE_URL") + if len(backendUrl) == 0 { + backendUrl = "http://shuffle-backend:5001" + } + + var workflowExecution *shuffle.WorkflowExecution + + streamResultUrl := fmt.Sprintf("%s/api/v1/streams/results", backendUrl) + topClient := shuffle.GetExternalClient(backendUrl) + requestData := shuffle.ActionResult { + Authorization: auth, + ExecutionId: executionId, + } + + data, err := json.Marshal(requestData) + if err != nil { + return workflowExecution, err + } + + req, err := http.NewRequest( + "POST", + streamResultUrl, + bytes.NewBuffer([]byte(data)), + ) + + newresp, err := topClient.Do(req) + if err != nil { + return workflowExecution, err + } + + defer newresp.Body.Close() + if newresp.StatusCode != 200 { + return workflowExecution, errors.New(fmt.Sprintf("Got bad status code from backend %d", newresp.StatusCode)) + } + + body, err := ioutil.ReadAll(newresp.Body) + if err != nil { + return workflowExecution, err + } + + err = json.Unmarshal(body, &workflowExecution) + if err != nil { + return workflowExecution, err + } + + return workflowExecution, nil +} func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { if request.Body == nil { @@ -2326,10 +2467,14 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { - log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) - resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) - return + log.Printf("[WARNING][%s] Failed to find execution in cache requesting backend (1): %s", actionResult.ExecutionId, err) + workflowExecution, err = getWorkerBackendExecution(actionResult.Authorization, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) + return + } } if workflowExecution.Authorization != actionResult.Authorization { @@ -2375,10 +2520,14 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl //log.Printf("[DEBUG][%s] IN WORKFLOWEXECUTION SUB!", actionResult.ExecutionId) workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId) if err != nil { - log.Printf("[ERROR] Failed getting execution cache: %s", err) - resp.WriteHeader(400) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution"}`))) - return + log.Printf("[WARNING][%s] Failed to find execution in cache requesting backend (2): %s", actionResult.ExecutionId, err) + workflowExecution, err = getWorkerBackendExecution(actionResult.Authorization, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR] Failed getting execution cache: %s", err) + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution"}`))) + return + } } resultLength := len(workflowExecution.Results) @@ -2429,10 +2578,14 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl if strings.Contains(fmt.Sprintf("%s", err), "Rerun this transaction") { workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId) if err != nil { - log.Printf("[ERROR][%s] Failed getting execution cache (2): %s", workflowExecution.ExecutionId, err) - resp.WriteHeader(400) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution (2)"}`))) - return + log.Printf("[WARNING][%s] Failed to find execution in cache requesting backend (3): %s", actionResult.ExecutionId, err) + workflowExecution, err = getWorkerBackendExecution(actionResult.Authorization, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR][%s] Failed getting execution cache (2): %s", workflowExecution.ExecutionId, err) + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution (2)"}`))) + return + } } resultLength = len(workflowExecution.Results) @@ -2494,11 +2647,16 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { - log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + if debug { + log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + } + //result(s)", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results)) err = setWorkflowExecution(ctx, *workflowExecution, dbSave) if err != nil { - resp.WriteHeader(401) + log.Printf("[ERROR][%s] Failed setting execution: %s", workflowExecution.ExecutionId, err) + + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution actionresult: %s"}`, err))) return } @@ -2507,7 +2665,10 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" { finished := shuffle.ValidateFinished(ctx, -1, *workflowExecution) if !finished { - log.Printf("[DEBUG][%s] Handling next node since it's not finished!", workflowExecution.ExecutionId) + if debug { + log.Printf("[DEBUG][%s] Handling next node since it's not finished!", workflowExecution.ExecutionId) + } + handleExecutionResult(*workflowExecution) } else { shutdownData, err := json.Marshal(workflowExecution) @@ -2589,7 +2750,7 @@ func sendSelfRequest(actionResult shuffle.ActionResult) { if err != nil { log.Printf("[ERROR][%s] Failed reading body: %s", actionResult.ExecutionId, err) } else { - log.Printf("[DEBUG][%s] NEWRESP (from backend): %s", actionResult.ExecutionId, string(body)) + log.Printf("[DEBUG][%s] NEWRESP (from backend - 2): %s", actionResult.ExecutionId, string(body)) } } } @@ -2742,10 +2903,14 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { - log.Printf("[INFO] Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err) - resp.WriteHeader(400) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`))) - return + log.Printf("[WARNING][%s] Failed to find execution in cache requesting backend (4): %s", actionResult.ExecutionId, err) + workflowExecution, err = getWorkerBackendExecution(actionResult.Authorization, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR] Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err) + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`))) + return + } } // Authorization is done here @@ -3465,7 +3630,9 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, log.Printf("[ERROR] Failed reading app request body body: %s", err) return err } else { - log.Printf("[DEBUG][%s] NEWRESP (from app): %s", workflowExecution.ExecutionId, string(body)) + if debug { + log.Printf("[DEBUG][%s] NEWRESP (from app): %s", workflowExecution.ExecutionId, string(body)) + } } return nil @@ -3611,7 +3778,7 @@ func getStreamResultsWrapper(client *http.Client, req *http.Request, workflowExe // Checks if a subflow is child of the startnode, as sub-subflows aren't working properly yet childNodes := shuffle.FindChildNodes(workflowExecution.Workflow, workflowExecution.Start, []string{}, []string{}) - log.Printf("[DEBUG] Looking for subflow in %#v to check execution pattern as child of %s", childNodes, workflowExecution.Start) + //log.Printf("[DEBUG] Looking for subflow in %#v to check execution pattern as child of %s", childNodes, workflowExecution.Start) subflowFound := false for _, childNode := range childNodes { for _, trigger := range workflowExecution.Workflow.Triggers { @@ -3693,8 +3860,174 @@ func getStreamResultsWrapper(client *http.Client, req *http.Request, workflowExe return environments, nil } +func checkStandaloneRun() { + // Check if the required argc/argv is set + //log.Printf("ARGS: %#v", os.Args) + if len(os.Args) < 4 { + return + } + + if os.Args[1] != "standalone" { + log.Printf("[ERROR] First argument should be 'standalone' to run worker standalone") + return + } + + if os.Args[2] == "" || len(os.Args[2]) != 36 { + log.Printf("[ERROR] Second argument should be the execution ID, with next being authorization") + return + } + + if os.Args[3] == "" || len(os.Args[3]) < 10 { + log.Printf("[ERROR] Third argument should be the authorization key") + return + } + + backendUrl := "https://shuffler.io" + if len(os.Args) > 4 { + backendUrl = os.Args[4] + } + + if !strings.Contains(backendUrl, "http") { + log.Printf("[ERROR] Backend URL should start with http:// or https://") + return + + } + + // Format: + // go run worker.go standalone + executionId := os.Args[2] + authorization := os.Args[3] + + os.Setenv("EXECUTIONID", executionId) + os.Setenv("AUTHORIZATION", authorization) + os.Setenv("BASE_URL", backendUrl) + + os.Setenv("STANDALONE_EXECUTION", "true") + + log.Printf("\n\n\n[DEBUG] Running worker in standalone mode with execution ID %s and authorization %s. Backend URL (default): %s\nThis means we will first RESET the execution results, then rerun it\n\n", executionId, authorization, backendUrl) + + // 1. Reset the execution after getting it + data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, executionId, authorization) + streamResultUrl := fmt.Sprintf("%s/api/v1/streams/results", backendUrl) + client := shuffle.GetExternalClient(streamResultUrl) + req, err := http.NewRequest( + "POST", + streamResultUrl, + bytes.NewBuffer([]byte(data)), + ) + + if err != nil { + log.Printf("[ERROR] Failed making request builder for backend: %s", err) + os.Exit(1) + } + + // Read the data and unmarshal it + newresp, err := client.Do(req) + if err != nil { + log.Printf("[ERROR] Failed standalone request in setup: %s", err) + os.Exit(1) + } + + defer newresp.Body.Close() + body, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("[ERROR] Failed reading body: %s", err) + os.Exit(1) + } + + if newresp.StatusCode != 200 { + log.Printf("[ERROR] Failed resetting execution: %s. Body: %s", newresp.Status, string(body)) + os.Exit(1) + } + + // Map to shuffle.Workflowexecution struct + workflowExecution := shuffle.WorkflowExecution{} + err = json.Unmarshal(body, &workflowExecution) + if err != nil { + log.Printf("[ERROR] Failed unmarshalling body: %s", err) + os.Exit(1) + } + + log.Printf("[DEBUG][%s] Got %d results with status %s. Running full reset IF status is not executing.", workflowExecution.ExecutionId, len(workflowExecution.Results), workflowExecution.Status) + + // Just continue as per usual? + //if workflowExecution.Status == "EXECUTING" { + // return + //} + + workflowExecution.Status = "EXECUTING" + + newResults := []shuffle.ActionResult{} + for _, result := range workflowExecution.Results { + if result.Status == "SKIPPED" { + newResults = append(newResults, result) + continue + } + + // This is to handle reruns of SINGLE actions + if result.Action.Category == "rerun" { + newResults = append(newResults, result) + continue + } + + // Anything else here. + } + + workflowExecution.Results = newResults + workflowExecution.Status = "EXECUTING" + workflowExecution.CompletedAt = 0 + + marshalledResult, err := json.Marshal(workflowExecution) + if err != nil { + log.Printf("[ERROR] Failed marshalling body: %s", err) + os.Exit(1) + } + + // Send a /api/v1/streams result back + // 1. Reset the execution after getting it + streamUrl := fmt.Sprintf("%s/api/v1/streams?reset=true", backendUrl) + req, err = http.NewRequest( + "POST", + streamUrl, + bytes.NewBuffer([]byte(marshalledResult)), + ) + + if err != nil { + log.Printf("[ERROR] Failed making request builder (2) for backend: %s", err) + os.Exit(1) + } + + // Read the data and unmarshal it + newresp, err = client.Do(req) + if err != nil { + log.Printf("[ERROR] Failed standalone request in setup: %s", err) + os.Exit(1) + } + + defer newresp.Body.Close() + body, err = ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("[ERROR] Failed reading body (2): %s", err) + os.Exit(1) + } + + if newresp.StatusCode != 200 { + log.Printf("[ERROR] Failed resetting execution (2): %s. Body: %s", newresp.Status, string(body)) + os.Exit(1) + } + + log.Printf("\n\n\n[DEBUG] Finished resetting execution %s. Body: %s. Starting execution.\n\n\n", newresp.Status, string(body)) + +} + // Initial loop etc func main() { + checkStandaloneRun() + + if os.Getenv("DEBUG") == "true" { + debug = true + } + /*** STARTREMOVE ***/ if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" { logsDisabled = "true" @@ -3715,7 +4048,7 @@ func main() { } } - log.Printf("[INFO] Setting up worker environment") + //log.Printf("[INFO] Setting up worker environment") sleepTime = 5 client := shuffle.GetExternalClient(baseUrl) @@ -3724,7 +4057,7 @@ func main() { } if baseimagename == "" { - log.Printf("[DEBUG] Setting baseimagename") + log.Printf("[DEBUG] Setting baseimagename to frikky/shuffle as it's empty (docker.io)") baseimagename = "frikky/shuffle" // Dockerhub //baseimagename = "shuffle" // Github (ghcr.io) } @@ -4250,9 +4583,10 @@ func getNetworkId(ctx context.Context, dockercli *dockerclient.Client) (string, networkFilter := filters.NewArgs() networkFilter.Add("name", swarmNetworkName) - networks, err := dockercli.NetworkList(ctx, types.NetworkListOptions{ + listOptions := network.ListOptions{ Filters: networkFilter, - }) + } + networks, err := dockercli.NetworkList(ctx, listOptions) if err != nil || len(networks) == 0 { return "", err @@ -4281,9 +4615,10 @@ func numberOfApps(ctx context.Context, dockercli *dockerclient.Client) int { networkFilter := filters.NewArgs() networkFilter.Add("name", swarmNetworkName) - networks, err := dockercli.NetworkList(ctx, types.NetworkListOptions{ + listOptions := network.ListOptions{ Filters: networkFilter, - }) + } + networks, err := dockercli.NetworkList(ctx, listOptions) if err != nil || len(networks) == 0 { return 0 diff --git a/functions/usecases/README.md b/functions/usecases/README.md deleted file mode 100644 index 553034c2..00000000 --- a/functions/usecases/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Mindmap exporter -Shuffle has a mindmap for Workflow use-cases. These can be changed and exported, with the most important piece being that they're explorable and editable. This has and will come in handy for us as we build it into the product. - -https://www.mindmeister.com/map/2172644474 - -## Editing the Mindmap -There are a few categories. To edit them, click the small plus next to the branch you want to change. - -## Exporting the Mindmap -Click "Export as RTF" in the top left corner of the URL. Download it there. - -## Generating the Shuffle-comaptible mindmap -1. Move the rtf file here -2. Rename it categories.rtf -3. Run the read_categories.py file (python3 read_categories.py) -4. You now have a file called categories.json locally with all the categories in JSON format, ready to be used in graphs. diff --git a/functions/usecases/categories.json b/functions/usecases/categories.json deleted file mode 100644 index e438a380..00000000 --- a/functions/usecases/categories.json +++ /dev/null @@ -1,260 +0,0 @@ -[ - { - "name": "1. Collect & Distribute", - "color": "#c51152", - "list": [ - { - "name": "2-way Ticket synchronization", - "items": {} - }, - { - "name": "Email management", - "items": { - "name": "Release a quarantined message", - "items": {} - } - }, - { - "name": "EDR to ticket", - "items": { - "name": "Get host information", - "items": {} - } - }, - { - "name": "SIEM to ticket", - "items": {} - }, - { - "name": "ChatOps", - "items": {} - }, - { - "name": "Threat Intel received", - "items": {} - }, - { - "name": "Domain investigation with LetsEncrypt", - "items": {} - }, - { - "name": "Botnet tracker", - "items": {} - }, - { - "name": "Get running containers", - "items": {} - }, - { - "name": "Assign tickets", - "items": {} - }, - { - "name": "Firewall alerts", - "items": { - "name": "URL filtering", - "items": {} - } - }, - { - "name": "IDS/IPS alerts", - "items": { - "name": "Manage policies", - "items": {} - } - }, - { - "name": "Deduplicate information", - "items": {} - }, - { - "name": "Correlate information", - "items": {} - } - ] - }, - { - "name": "2. Enrich", - "color": "#f4c20d", - "list": [ - { - "name": "Internal Enrichment", - "items": { - "name": "...", - "items": {} - } - }, - { - "name": "External historical Enrichment", - "items": { - "name": "...", - "items": {} - } - }, - { - "name": "Realtime", - "items": { - "name": "Analyze screenshots", - "items": {} - } - }, - { - "name": "Ticketing webhook verification", - "items": {} - } - ] - }, - { - "name": "3. Detect", - "color": "#3cba54", - "list": [ - { - "name": "Search SIEM (Sigma)", - "items": { - "name": "Endpoint", - "items": {} - } - }, - { - "name": "Search EDR (OSQuery)", - "items": {} - }, - { - "name": "Search emails (Phish)", - "items": { - "name": "Check headers and IOCs", - "items": {} - } - }, - { - "name": "Search IOCs (ioc-finder)", - "items": {} - }, - { - "name": "Search files (Yara)", - "items": {} - }, - { - "name": "Correlate tickets", - "items": {} - }, - { - "name": "Honeypot access", - "items": { - "name": "...", - "items": {} - } - } - ] - }, - { - "name": "4. Respond", - "color": "#4a148c", - "list": [ - { - "name": "Eradicate malware", - "items": {} - }, - { - "name": "Quarantine host(s)", - "items": {} - }, - { - "name": "Trigger scans", - "items": {} - }, - { - "name": "Update indicators (FW, EDR, SIEM...)", - "items": {} - }, - { - "name": "Autoblock activity when threat intel is received", - "items": {} - }, - { - "name": "Lock/Delete/Reset account", - "items": {} - }, - { - "name": "Lock vault", - "items": {} - }, - { - "name": "Increase authentication", - "items": {} - }, - { - "name": "Get policies from assets", - "items": {} - } - ] - }, - { - "name": "5. Verify", - "color": "#4885ed", - "list": [ - { - "name": "Discover vulnerabilities", - "items": {} - }, - { - "name": "Discover assets", - "items": {} - }, - { - "name": "Ensure policies are followed", - "items": {} - }, - { - "name": "Find Inactive users", - "items": {} - }, - { - "name": "Ensure access rights match HR systems", - "items": {} - }, - { - "name": "Ensure onboarding is followed", - "items": {} - }, - { - "name": "Third party apps in SaaS", - "items": {} - }, - { - "name": "Devices used for your cloud account", - "items": {} - }, - { - "name": "Too much access in GCP/Azure/AWS/ other clouds", - "items": {} - }, - { - "name": "Certificate validation", - "items": {} - }, - { - "name": "Monitor new DNS entries for domain with passive DNS", - "items": {} - }, - { - "name": "Monitor and track password dumps", - "items": {} - }, - { - "name": "Monitor for mentions of domain on darknet sites", - "items": {} - }, - { - "name": "Reporting", - "items": { - "name": "Monthly reports", - "items": { - "name": "...", - "items": {} - } - } - } - ] - } -] \ No newline at end of file diff --git a/functions/usecases/categories.rtf b/functions/usecases/categories.rtf deleted file mode 100644 index e19cf26a..00000000 --- a/functions/usecases/categories.rtf +++ /dev/null @@ -1,555 +0,0 @@ -{\rtf1\ansi\deff0\deflang2057\plain\fs24\fet1 -{\fonttbl -{\f0\froman Arial;} -} -{\info -{\createim\yr2022\mo2\dy20\hr1\min15} -} - -\paperw11907\paperh16840\margl1800\margr1800\margt1440\margb1440 -\slmult0\ltrpar\li0 -{\b\fs28 -Shuffle categories -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -1. Collect & Distribute -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -2-way Ticket synchronization -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Email management -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Attachments -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Manage senders -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Manage URLs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Encode & Decode URLs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Release a quarantined message -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -EDR to ticket -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Fetch incidents & events -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Quarantine files -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Quarantine host (respond) -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Get host information -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -SIEM to ticket -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -ChatOps -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Threat Intel received -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Domain investigation with LetsEncrypt -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Botnet tracker -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Get running containers -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Assign tickets -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Firewall alerts -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Block/accept policies -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Add addresses and ports to groups -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Support custom URL categories -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Fetch logs for specific address -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -URL filtering -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -IDS/IPS alerts -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Get/Fetch alerts -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Receive alerts real-time -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Get PCAP files -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Get network logs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Manage policies -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Deduplicate information -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Correlate information -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -3. Detect -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Search SIEM (Sigma) -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Network -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Endpoint -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Search EDR (OSQuery) -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Search emails (Phish) -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Check malware -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Check targeted -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Check headers and IOCs -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Search IOCs (ioc-finder) -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Search files (Yara) -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Correlate tickets -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Honeypot access -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -S3 Honeypot -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -SSH Honeypot -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -FTP honeypot -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Network honeypot -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -... -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -rich -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -5. Verify -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Discover vulnerabilities -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Discover assets -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Ensure policies are followed -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Find Inactive users -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Ensure access rights match HR systems -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Ensure onboarding is followed -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Third party apps in SaaS -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Devices used for your cloud account -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Too much access in GCP/Azure/AWS/ other clouds -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Certificate validation -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Monitor new DNS entries for domain with passive DNS -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Monitor and track password dumps -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Monitor for mentions of domain on darknet sites -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Reporting -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Automation time saved -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Automation money saved -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Incident response report -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Department cost -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Monthly reports -} -\par\pard\plain -\slmult0\ltrpar\li800 -{\fs24 -EDR alerts -} -\par\pard\plain -\slmult0\ltrpar\li800 -{\fs24 -SIEM alerts -} -\par\pard\plain -\slmult0\ltrpar\li800 -{\fs24 -Emails quarantined -} -\par\pard\plain -\slmult0\ltrpar\li800 -{\fs24 -... -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -4. Respond -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Eradicate malware -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Quarantine host(s) -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Trigger scans -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Update indicators (FW, EDR, SIEM...) -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Autoblock activity when threat intel is received -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Lock/Delete/Reset account -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Lock vault -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Increase authentication -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Get policies from assets -} -\par\pard\plain -\slmult0\ltrpar\li200 -{\fs24 -2. Enrich -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Internal Enrichment -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Users -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Hostnames -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -IPs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Departments -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Role -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Software -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -... -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -External historical Enrichment -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -IPs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -URLs -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Hashes -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Files -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -... -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Realtime -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -File detonation -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -URL detonation -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -PCAP analysis -} -\par\pard\plain -\slmult0\ltrpar\li600 -{\fs24 -Analyze screenshots -} -\par\pard\plain -\slmult0\ltrpar\li400 -{\fs24 -Ticketing webhook verification -} -\par\pard\plain -} \ No newline at end of file diff --git a/functions/usecases/read_categories.py b/functions/usecases/read_categories.py deleted file mode 100644 index 65df43c9..00000000 --- a/functions/usecases/read_categories.py +++ /dev/null @@ -1,66 +0,0 @@ -data = "" -with open("categories.rtf", "r") as tmp: - data = tmp.read() - -fixed_json = [] -linearity = 0 -heading = "" -subheading = "" -subsubheading = "" - -cnt = -1 -subcnt = -1 - -colors = ["#c51152", "#3cba54", "#4885ed", "#4a148c", "#f4c20d"] -for line in data.split("\n"): - if line == "rich": - continue - - if "li" in line: - lisplit = line.split("\\") - try: - linearity = int(lisplit[-1][2]) - except: - pass - - #print("Linearity: %s" % linearity) - - if line.startswith("{") or line.startswith("}"): - continue - - if line.startswith("\\"): - continue - - if linearity == 0: - continue - - if linearity == 2: - #if cnt >= 0: - # for key, value in fixed_json[cnt].items(): - # print(key, value) - - - cnt += 1 - subcnt = -1 - fixed_json.append({"name": line, "color": colors[cnt], "list": []}) - heading = line - elif linearity == 4: - subheading = line - fixed_json[cnt]["list"].append({"name": line, "items": {}}) - subcnt += 1 - elif linearity == 6: - fixed_json[cnt]["list"][subcnt]["items"] = {"name": line, "items": {}} - elif linearity == 8: - fixed_json[cnt]["list"][subcnt]["items"]["items"] = {"name": line, "items": {}} - else: - print("No handler for %s" % line) - -#print(line) -#print(data) -import json -filename = "categories.json" -fixed_json.sort(key=lambda x: x["name"]) -with open(filename, "w+") as tmp: - tmp.write(json.dumps(fixed_json, indent=4)) - -print("Wrote to file %s" % filename)