Merge branch 'nightly' into kubernetes-labels

This commit is contained in:
Pascal Sthamer
2025-03-24 18:22:26 +01:00
committed by GitHub
152 changed files with 16408 additions and 3350 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ BACKEND_HOSTNAME=shuffle-backend
BACKEND_PORT=5001
FRONTEND_PORT=3001
FRONTEND_PORT_HTTPS=3443
AUTH_FOR_ORBORUS =
AUTH_FOR_ORBORUS=
# CHANGE THIS IF YOU WANT GOOD LOCAL EXECUTIONS:
OUTER_HOSTNAME=shuffle-backend
+11
View File
@@ -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"
+1 -1
View File
@@ -35,7 +35,7 @@ sudo swapoff -a # Disable swap
4. Run docker-compose.
```bash
docker compose up -d
docker-compose up -d
```
5. Recommended for Opensearch to work well
+5 -5
View File
@@ -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.1
experimental: true
- app: backend
path: backend
version: nightly
version: 2.0.1
experimental: true
- app: orborus
path: functions/onprem/orborus
version: nightly
version: 2.0.1
experimental: true
- app: worker
path: functions/onprem/worker
version: nightly
version: 2.0.1
experimental: true
steps:
- name: Checkout
+71
View File
@@ -0,0 +1,71 @@
name: Helm Release
on:
release:
types: [published]
branches:
- main
- nightly
push:
branches:
- nightly
paths:
- "functions/kubernetes/charts/**"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install apt dependencies
run: |
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https -y --no-install-recommends
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
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 }}"
else
CHART_VERSION="0.0.0-nightly-untagged-latest"
APP_VERSION="nightly"
fi
echo "CHART_VERSION set to ${CHART_VERSION}. Validating..."
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
SEMVER_REGEX="^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
if echo "${CHART_VERSION}" | grep -Pq "${SEMVER_REGEX}"; then
echo "${CHART_VERSION} is a valid SemVer string";
else
echo "${CHART_VERSION} is an invalid SemVer string";
exit 1;
fi
echo "CHART_VERSION=${CHART_VERSION}" >> $GITHUB_OUTPUT
echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_OUTPUT
- name: Update helm dependencies
run: helm dependency update ./functions/kubernetes/charts/shuffle
- name: Package Helm chart
run: helm package ./functions/kubernetes/charts/shuffle --version "${CHART_VERSION}" --app-version="${APP_VERSION}" --destination ./functions/kubernetes/charts
- name: Login to OCI registry (ghcr.io)
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
+1 -3
View File
@@ -3,6 +3,7 @@ on:
release:
types: [published]
branches:
- main
- nightly
jobs:
@@ -19,9 +20,6 @@ jobs:
- app: backend
path: backend
experimental: true
- app: app_sdk
path: backend/app_sdk
experimental: true
- app: orborus
path: functions/onprem/orborus
experimental: true
+56 -20
View File
@@ -5,6 +5,7 @@ on:
workflows: ["dockerbuild"]
types:
- completed
workflow_dispatch:
jobs:
build:
@@ -13,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: mkdir shuffle-database && chmod -R 777 shuffle-database
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
@@ -54,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
@@ -95,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')
@@ -127,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
+1 -5
View File
@@ -4,10 +4,6 @@
Shuffle Automation
[![CodeQL](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml)
[![Autobuild](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml)
[![Deploy to AWS](https://d1.awsstatic.com/cloudformation-deploy-to-aws-button.png)](https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=Shuffle-Instance&templateURL=https://shuffle-public-amis.s3.eu-north-1.amazonaws.com/template.yaml)
</h1><h4 align="center">
[Shuffle](https://shuffler.io) is an open source automation platform, built for and by the security professionals. Security operations is complex, but it doesn't have to be. Built to work well with MSSP's and other service providers in mind.
@@ -29,7 +25,7 @@ Follow us on Twitter at [@shuffleio](https://twitter.com/shuffleio).
## Try it
* Self-hosted: Check out the [installation guide](https://github.com/shuffle/shuffle/blob/master/.github/install-guide.md)
* Cloud: Register at https://shuffler.io/register and get cooking (missing a lot of features)
* Cloud: Register at https://shuffler.io/register and get cooking
Please consider [sponsoring](https://github.com/sponsors/frikky) the project if you want to see more rapid development.
+3 -4
View File
@@ -1,4 +1,4 @@
FROM golang:1.22 as builder
FROM golang:1.23 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
-9
View File
@@ -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
-7
View File
@@ -984,11 +984,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}`))
}
+13 -11
View File
@@ -1,6 +1,8 @@
module shuffle
go 1.22.2
go 1.23.0
toolchain go1.23.7
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
@@ -18,11 +20,10 @@ require (
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.7.92
golang.org/x/crypto v0.32.0
github.com/shuffle/shuffle-shared v0.8.19
golang.org/x/crypto v0.36.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
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
@@ -37,6 +38,7 @@ require (
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/scheduler v1.10.6 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -47,7 +49,6 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // 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/containerd/log v0.1.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -107,23 +108,24 @@ require (
github.com/skeema/knownhosts v1.3.0 // 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/xanzy/ssh-agent v0.3.3 // indirect
go.opencensus.io v0.24.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/exporters/otlp/otlptrace v1.3.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 v0.11.0 // indirect
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/net v0.37.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/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.8 // indirect
+62 -146
View File
@@ -15,8 +15,6 @@ cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKF
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -37,8 +35,6 @@ 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/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=
@@ -48,21 +44,15 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
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.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/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/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/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=
@@ -87,30 +77,29 @@ github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8b
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/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/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/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/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/containerd/containerd v1.6.26 h1:VVfrE6ZpyisvB1fzoY8Vkiq4sy+i5oF4uk7zu03RaHs=
github.com/containerd/containerd v1.6.26/go.mod h1:I4TRdsdoo5MlKob5khDJS2EPT1l1oMNaE2MBm6FrwxM=
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/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.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/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.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -118,30 +107,26 @@ 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/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/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.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug=
github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
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.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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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=
@@ -150,28 +135,24 @@ github.com/fsouza/go-dockerclient v1.11.0 h1:4ZAk6W7rPAtPXm7198EFqA5S68rwnNQORxl
github.com/fsouza/go-dockerclient v1.11.0/go.mod h1:0I3TQCRseuPTzqlY4Y3ajfsg2VAdMQoazrkxJTiJg8s=
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.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
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-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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
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-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-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
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=
@@ -206,6 +187,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
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=
@@ -222,6 +204,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/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=
@@ -267,7 +250,6 @@ 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=
@@ -276,7 +258,6 @@ 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=
@@ -288,7 +269,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
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=
@@ -296,7 +276,6 @@ 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/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
@@ -307,8 +286,6 @@ 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/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=
@@ -322,12 +299,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
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/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
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/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M=
@@ -336,18 +311,17 @@ github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsr
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
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/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=
@@ -357,31 +331,20 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
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/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.77 h1:KKtM50xW2DLuRHINxhp3uXrNH0AhiwkeiiU93a8fB3A=
github.com/shuffle/shuffle-shared v0.6.77/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.8.19 h1:HXqU62sPhVzv9MeJnA5ZpPYwwbitVz1EtMMABbY3t74=
github.com/shuffle/shuffle-shared v0.8.19/go.mod h1:NruHSAscDsW595wpK2r7MeHPGspUEKRNvBpcN1iGbHI=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/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/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/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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -393,9 +356,8 @@ 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/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
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.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=
@@ -411,49 +373,38 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
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.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
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/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.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/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
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.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
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 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=
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.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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
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=
@@ -462,6 +413,8 @@ 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=
@@ -481,9 +434,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=
@@ -501,6 +451,7 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL
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=
@@ -508,21 +459,14 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -533,14 +477,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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.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=
@@ -553,34 +493,27 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
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=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
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=
@@ -591,11 +524,8 @@ 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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=
@@ -611,9 +541,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=
@@ -629,9 +557,6 @@ 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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -670,15 +595,12 @@ 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-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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -689,9 +611,10 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
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.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
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=
@@ -705,8 +628,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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.27.1/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -717,15 +639,10 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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.3/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=
@@ -733,9 +650,8 @@ 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.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
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=
+56 -10
View File
@@ -31,12 +31,13 @@ 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"
"github.com/go-git/go-git/v5/storage/memory"
gitProxy "github.com/go-git/go-git/v5/plumbing/transport"
http2 "github.com/go-git/go-git/v5/plumbing/transport/http"
// Random
xj "github.com/basgys/goxml2json"
@@ -46,7 +47,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
@@ -3079,8 +3081,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 +3157,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 +3293,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.
@@ -3349,11 +3369,27 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
}
}
log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID)
if len(user.Id) > 0 {
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
@@ -4343,15 +4379,15 @@ func runInitEs(ctx context.Context) {
}
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 +4458,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(
@@ -5185,6 +5221,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")
+14 -508
View File
@@ -33,9 +33,11 @@ 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/go-git/go-git/v5/plumbing"
http2 "github.com/go-git/go-git/v5/plumbing/transport/http"
//http2 "gopkg.in/src-d/go-git.v5/plumbing/transport/http"
//http2 "github.com/go-git/go-git/plumbing/transport/http"
//"github.com/gorilla/websocket"
//"google.golang.org/appengine"
@@ -1032,10 +1034,14 @@ 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))
shuffle.DeleteCache(ctx, fmt.Sprintf("%s_%s", user.Username, fileId))
log.Printf("[DEBUG] Cleared workflow cache for %s (%s)", user.Username, user.Id)
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 +1056,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 +1100,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 +1141,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 +1151,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 +1403,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)
+6 -6
View File
@@ -1,6 +1,6 @@
services:
frontend:
image: ghcr.io/shuffle/shuffle-frontend:1.4.2
image: ghcr.io/shuffle/shuffle-frontend:latest
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -14,7 +14,7 @@ services:
depends_on:
- backend
backend:
image: ghcr.io/shuffle/shuffle-backend:1.4.2
image: ghcr.io/shuffle/shuffle-backend:latest
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:1.4.0
image: ghcr.io/shuffle/shuffle-orborus:latest
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_SWARM_CONFIG=run
- SHUFFLE_LOGS_DISABLED=true
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:1.4.2
- SHUFFLE_SWARM_CONFIG=run
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest
env_file: .env
restart: unless-stopped
security_opt:
- seccomp:unconfined
opensearch:
image: opensearchproject/opensearch:2.14.0
image: opensearchproject/opensearch:2.19.1
hostname: shuffle-opensearch
container_name: shuffle-opensearch
environment:
+74
View File
@@ -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
Executable → Regular
+2 -1
View File
@@ -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"
}
}
@@ -0,0 +1,6 @@
<svg width="45" height="56" viewBox="0 0 45 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.866 31.366L22.2583 37.366L13.3454 52.8036L11.2901 46.9667C10.9625 46.0363 10.0053 45.4837 9.03569 45.6651L2.95314 46.8036L11.866 31.366Z" stroke="#2BC07E" stroke-width="2"/>
<path d="M33.759 31.366L23.3667 37.366L32.2796 52.8036L34.3349 46.9667C34.6625 46.0363 35.6197 45.4837 36.5893 45.6651L42.6719 46.8036L33.759 31.366Z" stroke="#2BC07E" stroke-width="2"/>
<path d="M18.6381 6.27038C20.6383 3.8382 24.3617 3.8382 26.3619 6.27038L26.7961 6.79841C28.1024 8.38685 30.0411 9.32049 32.0974 9.35141L32.781 9.36168C35.9296 9.40902 38.2511 12.3201 37.5966 15.4003L37.4546 16.069C37.0271 18.0807 37.5059 20.1786 38.7639 21.8056L39.182 22.3464C41.1082 24.8376 40.2796 28.4677 37.4634 29.8765L36.852 30.1823C35.0127 31.1024 33.671 32.7847 33.1833 34.7826L33.0212 35.4468C32.2744 38.5059 28.9197 40.1215 26.0624 38.798L25.442 38.5106C23.5759 37.6463 21.4241 37.6463 19.558 38.5106L18.9376 38.798C16.0803 40.1215 12.7256 38.5059 11.9788 35.4468L11.8167 34.7826C11.329 32.7847 9.98734 31.1024 8.14805 30.1823L7.53663 29.8765C4.72036 28.4677 3.89182 24.8376 5.81795 22.3464L6.23611 21.8056C7.49405 20.1786 7.97288 18.0807 7.54544 16.069L7.40335 15.4003C6.74887 12.3201 9.07037 9.40902 12.219 9.36168L12.9026 9.35141C14.9589 9.32049 16.8976 8.38685 18.2039 6.79841L17.4316 6.16323L18.2039 6.79841L18.6381 6.27038Z" fill="#2F2F2F" stroke="#2BC07E" stroke-width="2"/>
<path d="M30.5 18.5L21 28L16 23" stroke="#2BC07E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,5 @@
<svg width="54" height="52" viewBox="0 0 54 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.9999 31.4723C38.007 34.1853 37.3731 36.8617 36.1499 39.2834C34.6996 42.1853 32.47 44.6261 29.7108 46.3324C26.9517 48.0387 23.7719 48.9432 20.5277 48.9444C17.9537 48.9512 15.4126 48.3809 13.0908 47.2778C12.8499 47.1634 12.5744 47.1419 12.3214 47.2262L2.89737 50.3675C2.11561 50.6281 1.37187 49.8844 1.63246 49.1026L4.77381 39.6786C4.85812 39.4256 4.83665 39.1501 4.72223 38.9092C3.6191 36.5874 3.04884 34.0463 3.05555 31.4723C3.05681 28.2281 3.96125 25.0483 5.66758 22.2892C7.37391 19.53 9.81473 17.3004 12.7166 15.8501C15.1383 14.6269 17.8147 13.993 20.5277 14.0001H21.5555C25.84 14.2364 29.8868 16.0448 32.921 19.079C35.9552 22.1132 37.7636 26.16 37.9999 30.4445V31.4723Z" stroke="#4FB1E8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M39 32L38.0926 33.7823C38.6627 34.0726 39.3373 34.0726 39.9074 33.7823L39 32ZM39 32C39.9074 33.7823 39.9079 33.782 39.9085 33.7817L39.91 33.781L39.9136 33.7791L39.9239 33.7739L39.9562 33.7571C39.9829 33.7432 40.0199 33.7237 40.0664 33.6989C40.1593 33.6492 40.2906 33.5778 40.4547 33.4853C40.7826 33.3004 41.2432 33.0306 41.7925 32.6811C42.8869 31.9846 44.3541 30.9586 45.8304 29.6433C48.7043 27.083 52 23.078 52 18V8.2C52 7.37067 51.4882 6.62739 50.7134 6.33156L39.7134 2.13156C39.254 1.95615 38.746 1.95615 38.2866 2.13156L27.2866 6.33156C26.5118 6.62739 26 7.37067 26 8.2V18C26 23.078 29.2957 27.083 32.1696 29.6433C33.6459 30.9586 35.1131 31.9846 36.2075 32.6811C36.7568 33.0306 37.2174 33.3004 37.5453 33.4853C37.7094 33.5778 37.8407 33.6492 37.9336 33.6989C37.9801 33.7237 38.0171 33.7432 38.0438 33.7571L38.0761 33.7739L38.0864 33.7791L38.09 33.781L38.0915 33.7817C38.0921 33.782 38.0926 33.7823 39 32Z" fill="#2F2F2F" stroke="#2F2F2F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40.1819 30.1501C39.6936 30.4608 39.2864 30.6997 39 30.8615C38.7136 30.6997 38.3064 30.4608 37.8181 30.1501C36.8185 29.5139 35.4896 28.5832 34.1652 27.4033C31.4771 25.0085 29 21.761 29 18V8.8886L39 5.07041L49 8.8886V18C49 21.761 46.5229 25.0085 43.8348 27.4033C42.5104 28.5832 41.1815 29.5139 40.1819 30.1501Z" stroke="#4FB1E8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2326 41.4787H19.2326L19.2326 41.47C19.2313 40.1671 19.6607 38.9 20.4546 37.865C21.2485 36.8301 22.3625 36.0851 23.6242 35.7458C24.8858 35.4065 26.2244 35.4919 27.4325 35.9887C28.6405 36.4855 29.6503 37.3659 30.3056 38.4931L30.5302 38.8796L30.9393 38.6995C33.4578 37.591 35.237 36.1436 36.3734 34.1074C37.5026 32.084 37.9652 29.5351 37.9652 26.2827V19.274C37.9652 14.5199 36.6216 10.455 34.1879 7.56945C31.7474 4.6759 28.2394 3.00436 24.0012 3.00436C19.7629 3.00436 16.2549 4.67653 13.8145 7.57034C11.3808 10.4561 10.0371 14.521 10.0371 19.274V27.4501C10.0367 27.7819 9.90438 28.1002 9.66889 28.3351C9.43337 28.5701 9.11382 28.7025 8.78025 28.703H6.43988C4.86506 28.7013 3.35543 28.0765 2.242 26.966C1.1287 25.8556 0.502381 24.3503 0.5 22.7803V20.4447C0.501784 18.874 1.12781 17.3678 2.24116 16.2566C3.35447 15.1454 4.86421 14.5199 6.43943 14.5174C6.43964 14.5174 6.43985 14.5174 6.44006 14.5174L7.55861 14.5174H7.96354L8.04769 14.1213C9.77814 5.97693 15.6661 0.5 24.0012 0.5C32.3361 0.5 38.2218 5.97684 39.9523 14.1213L40.0365 14.5174H40.4414L41.5599 14.5174C41.5602 14.5174 41.5604 14.5174 41.5606 14.5174C43.1354 14.5199 44.6448 15.1451 45.7581 16.2558C46.8714 17.3667 47.4977 18.8724 47.5 20.4428V22.7778C47.4983 24.3483 46.8722 25.8543 45.7588 26.9652C44.6453 28.0762 43.1353 28.7013 41.5601 28.703H40.8371H40.3532L40.3374 29.1866C40.2503 31.8522 39.354 34.4287 37.7668 36.5749C36.1796 38.7212 33.9764 40.3361 31.4486 41.2057L31.1171 41.3197L31.1114 41.6704C31.0859 43.2413 30.436 44.7381 29.3042 45.8313C28.1724 46.9246 26.6514 47.5247 25.0758 47.4992C23.5002 47.4738 21.9995 46.8249 20.9038 45.6957C19.8081 44.5666 19.2071 43.0496 19.2326 41.4787ZM21.745 41.4701V41.4705C21.745 42.1473 21.9461 42.8087 22.3228 43.3712C22.6995 43.9337 23.2348 44.3719 23.8609 44.6306C24.4869 44.8894 25.1758 44.9571 25.8403 44.8252C26.5049 44.6933 27.1154 44.3677 27.5948 43.8895C28.0741 43.4112 28.4007 42.8017 28.533 42.138C28.6654 41.4743 28.5974 40.7864 28.3379 40.1613C28.0784 39.5362 27.639 39.0021 27.0754 38.6264C26.5119 38.2507 25.8495 38.0502 25.172 38.0502H25.1716C24.2634 38.051 23.3923 38.4112 22.7498 39.0523C22.1072 39.6934 21.7457 40.563 21.745 41.4701ZM40.4776 25.7009V26.2009H40.9776H41.5607C42.4693 26.2009 43.341 25.841 43.9842 25.1999C44.6273 24.5587 44.9892 23.6887 44.99 22.781V22.7806V20.4444C44.99 19.5368 44.6288 18.6665 43.9861 18.0248C43.3435 17.3831 42.4721 17.0225 41.5634 17.0218H41.563H40.9776H40.4776V17.5218V25.7009ZM3.01237 22.7806L3.01237 22.7815C3.01382 23.6881 3.37566 24.557 4.01816 25.1975L4.37117 24.8434L4.01816 25.1975C4.66062 25.838 5.53122 26.1979 6.43894 26.1986H6.43934H7.02473H7.52473V25.6986V17.5218V17.0218H7.02473H6.43934H6.43894C5.53027 17.0225 4.65884 17.3831 4.01622 18.0248C3.37357 18.6665 3.01237 19.5368 3.01237 20.4444L3.01237 22.7806Z" fill="#FF8544" stroke="#2F2F2F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="65" height="45" viewBox="0 0 65 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.5445 38.9971V36.0579C28.5445 34.4989 27.9252 33.0037 26.8228 31.9013C25.7204 30.7989 24.2253 30.1796 22.6662 30.1796H10.9096C9.35055 30.1796 7.85537 30.7989 6.75297 31.9013C5.65057 33.0037 5.03125 34.4989 5.03125 36.0579V38.9971" stroke="#806BFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.7885 25.8205C20.035 25.8205 22.6668 23.1887 22.6668 19.9422C22.6668 16.6957 20.035 14.0638 16.7885 14.0638C13.542 14.0638 10.9102 16.6957 10.9102 19.9422C10.9102 23.1887 13.542 25.8205 16.7885 25.8205Z" stroke="#806BFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 43.9362H32.575" stroke="#806BFF" stroke-width="2" stroke-linecap="round"/>
<path d="M16.5742 8.16418V5C16.5742 2.79086 18.3651 1 20.5742 1H59.5742C61.7834 1 63.5742 2.79086 63.5742 5V29C63.5742 31.2091 61.7834 33 59.5742 33H33.3907" stroke="#806BFF" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="66" height="28" viewBox="0 0 66 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.25391 27.1562C5.13411 27.0521 3.82552 27 2.32812 27H0.785156L9.28125 3.19141C9.65885 2.1237 9.84766 1.50521 9.84766 1.33594C9.84766 1.15365 9.84115 1.04297 9.82812 1.00391L9.84766 0.964844C10.694 1.00391 11.5924 1.02344 12.543 1.02344C13.3242 1.02344 14.2617 1.01042 15.3555 0.984375L15.375 1.02344C15.349 1.07552 15.3359 1.14714 15.3359 1.23828C15.3359 1.48568 15.5312 2.14323 15.9219 3.21094L24.5938 27.0586C24.3333 27.0456 24.0273 27.0391 23.6758 27.0391C23.6758 27.0391 23.3372 27.026 22.6602 27C22.3346 27 22.0417 27 21.7812 27H18.7539L16.918 21.375H8.08984L6.25391 27.1562ZM15.3945 16.6875L12.4453 7.60547L9.57422 16.6875H15.3945ZM27.3867 0.886719C29.1966 0.652344 31.0521 0.535156 32.9531 0.535156C36.4948 0.535156 39.1836 1.36849 41.0195 3.03516C42.8685 4.6888 43.793 7.16927 43.793 10.4766C43.793 13.4323 42.7839 15.7695 40.7656 17.4883C38.8776 19.1029 36.4492 19.9102 33.4805 19.9102C33.1419 19.9102 32.7708 19.8906 32.3672 19.8516V27H27.3867V0.886719ZM33.5391 5.24219C33.1484 5.24219 32.7578 5.25521 32.3672 5.28125V15.5547C32.7839 15.6068 33.2917 15.6328 33.8906 15.6328C34.4896 15.6328 35.1146 15.5091 35.7656 15.2617C36.4167 15.0013 36.9635 14.6432 37.4062 14.1875C38.3307 13.237 38.793 11.987 38.793 10.4375C38.793 6.97396 37.0417 5.24219 33.5391 5.24219ZM48.4023 0.945312C49.7044 0.997396 50.4922 1.02344 50.7656 1.02344H52.1719C52.862 1.02344 53.4154 0.971354 53.832 0.867188L53.8125 27H48.4023V0.945312ZM59.9258 27C60.043 26.2057 60.1016 25.5026 60.1016 24.8906C60.1016 24.2786 60.0951 23.862 60.082 23.6406C60.082 23.4062 60.0755 23.1784 60.0625 22.957C60.0495 22.7357 60.0365 22.5273 60.0234 22.332L59.9648 21.8438H65.6289C65.5898 22.3385 65.5703 22.7422 65.5703 23.0547L65.5312 23.875C65.5312 24.1224 65.5312 24.5911 65.5312 25.2812C65.5312 25.9583 65.5638 26.5312 65.6289 27H59.9258Z" fill="#2BC07E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,6 @@
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.9263 14.5851V13.0757C8.9263 12.275 9.24436 11.5071 9.81052 10.941C10.3767 10.3748 11.1445 10.0568 11.9452 10.0568H17.983C18.7837 10.0568 19.5516 10.3748 20.1177 10.941C20.6839 11.5071 21.002 12.275 21.002 13.0757V14.5851" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.9635 7.03783C13.2962 7.03783 11.9446 5.68621 11.9446 4.01891C11.9446 2.35161 13.2962 1 14.9635 1C16.6308 1 17.9824 2.35161 17.9824 4.01891C17.9824 5.68621 16.6308 7.03783 14.9635 7.03783Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.99923 12.887V11.6292C0.99923 10.9619 1.26428 10.3221 1.73608 9.85025C2.20788 9.37846 2.84777 9.1134 3.51499 9.1134L7.98047 9.1134" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.03111 6.59763C4.6417 6.59763 3.51535 5.47128 3.51535 4.08186C3.51535 2.69245 4.6417 1.5661 6.03111 1.5661C7.42053 1.5661 8.54688 2.69245 8.54688 4.08186C8.54688 5.47128 7.42053 6.59763 6.03111 6.59763Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,6 @@
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.9263 14.5851V13.0757C8.9263 12.275 9.24436 11.5071 9.81052 10.941C10.3767 10.3748 11.1445 10.0568 11.9452 10.0568H17.983C18.7837 10.0568 19.5516 10.3748 20.1177 10.941C20.6839 11.5071 21.002 12.275 21.002 13.0757V14.5851" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.9635 7.03783C13.2962 7.03783 11.9446 5.68621 11.9446 4.01891C11.9446 2.35161 13.2962 1 14.9635 1C16.6308 1 17.9824 2.35161 17.9824 4.01891C17.9824 5.68621 16.6308 7.03783 14.9635 7.03783Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.99923 12.887V11.6292C0.99923 10.9619 1.26428 10.3221 1.73608 9.85025C2.20788 9.37846 2.84777 9.1134 3.51499 9.1134L7.98047 9.1134" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.03111 6.59763C4.6417 6.59763 3.51535 5.47128 3.51535 4.08186C3.51535 2.69245 4.6417 1.5661 6.03111 1.5661C7.42053 1.5661 8.54688 2.69245 8.54688 4.08186C8.54688 5.47128 7.42053 6.59763 6.03111 6.59763Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,7 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3214 5H11.4351H12.5488" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="0.501953" y="0.5" width="15" height="19" rx="1.5" stroke="#9E9E9E"/>
<path d="M3.63903 12H12.5488" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.63903 15.6666H12.5488" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3214 8.33331H11.4351H12.5488" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 590 B

@@ -0,0 +1,7 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3214 5H11.4351H12.5488" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="0.501953" y="0.5" width="15" height="19" rx="1.5" stroke="#FF8544"/>
<path d="M3.63903 12H12.5488" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.63903 15.6666H12.5488" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3214 8.33331H11.4351H12.5488" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 590 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.502 8.50003L17.502 8.50133C17.5052 9.74245 17.2152 10.9668 16.6557 12.0746L16.6547 12.0765C15.9906 13.4052 14.9698 14.5228 13.7064 15.3041C12.4431 16.0853 10.9872 16.4995 9.50176 16.5L9.50065 16.5C8.25953 16.5033 7.0352 16.2133 5.92738 15.6537C5.80838 15.5936 5.67031 15.5835 5.54384 15.6257L0.792523 17.2095L2.37629 12.4581C2.41845 12.3317 2.40836 12.1936 2.34825 12.0746C1.78869 10.9668 1.49872 9.74245 1.50195 8.50133L1.50195 8.50022C1.50253 7.01482 1.91665 5.55891 2.69792 4.29556C3.4792 3.03222 4.59678 2.01134 5.92548 1.34728L5.92548 1.34728L5.92738 1.34633C7.0352 0.786764 8.25953 0.496791 9.50065 0.500028H9.50195H9.98796C11.9444 0.611298 13.7917 1.43858 15.1775 2.82444C16.5634 4.21031 17.3907 6.05759 17.502 8.01402L17.502 8.50003Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.502 8.50003L17.502 8.50133C17.5052 9.74245 17.2152 10.9668 16.6557 12.0746L16.6547 12.0765C15.9906 13.4052 14.9698 14.5228 13.7064 15.3041C12.4431 16.0853 10.9872 16.4995 9.50176 16.5L9.50065 16.5C8.25953 16.5033 7.0352 16.2133 5.92738 15.6537C5.80838 15.5936 5.67031 15.5835 5.54384 15.6257L0.792523 17.2095L2.37629 12.4581C2.41845 12.3317 2.40836 12.1936 2.34825 12.0746C1.78869 10.9668 1.49872 9.74245 1.50195 8.50133L1.50195 8.50022C1.50253 7.01482 1.91665 5.55891 2.69792 4.29556C3.4792 3.03222 4.59678 2.01134 5.92548 1.34728L5.92548 1.34728L5.92738 1.34633C7.0352 0.786764 8.25953 0.496791 9.50065 0.500028H9.50195H9.98796C11.9444 0.611298 13.7917 1.43858 15.1775 2.82444C16.5634 4.21031 17.3907 6.05759 17.502 8.01402L17.502 8.50003Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.002 1H3.00195C2.47152 1 1.96281 1.21071 1.58774 1.58579C1.21267 1.96086 1.00195 2.46957 1.00195 3V19C1.00195 19.5304 1.21267 20.0391 1.58774 20.4142C1.96281 20.7893 2.47152 21 3.00195 21H15.002C15.5324 21 16.0411 20.7893 16.4162 20.4142C16.7912 20.0391 17.002 19.5304 17.002 19V8L10.002 1Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.002 1V8H17.002" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

@@ -0,0 +1,4 @@
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.002 1H3.00195C2.47152 1 1.96281 1.21071 1.58774 1.58579C1.21267 1.96086 1.00195 2.46957 1.00195 3V19C1.00195 19.5304 1.21267 20.0391 1.58774 20.4142C1.96281 20.7893 2.47152 21 3.00195 21H15.002C15.5324 21 16.0411 20.7893 16.4162 20.4142C16.7912 20.0391 17.002 19.5304 17.002 19V8L10.002 1Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.002 1V8H17.002" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.002 19C14.9725 19 19.002 14.9706 19.002 10C19.002 5.02944 14.9725 1 10.002 1C5.03139 1 1.00195 5.02944 1.00195 10C1.00195 14.9706 5.03139 19 10.002 19Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.38281 7.3C7.59441 6.6985 8.01205 6.1913 8.56177 5.86822C9.11149 5.54514 9.75782 5.42704 10.3863 5.53484C11.0147 5.64264 11.5847 5.96937 11.9954 6.45718C12.406 6.94498 12.6308 7.56237 12.6298 8.2C12.6298 10 9.92981 10.9 9.92981 10.9" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.002 14.5H10.011" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.002 19C14.9725 19 19.002 14.9706 19.002 10C19.002 5.02944 14.9725 1 10.002 1C5.03139 1 1.00195 5.02944 1.00195 10C1.00195 14.9706 5.03139 19 10.002 19Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.38281 7.3C7.59441 6.6985 8.01205 6.1913 8.56177 5.86822C9.11149 5.54514 9.75782 5.42704 10.3863 5.53484C11.0147 5.64264 11.5847 5.96937 11.9954 6.45718C12.406 6.94498 12.6308 7.56237 12.6298 8.2C12.6298 10 9.92981 10.9 9.92981 10.9" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.002 14.5H10.011" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

+11
View File
@@ -0,0 +1,11 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="56" height="56" rx="4" fill="#2F2F2F"/>
<g clip-path="url(#clip0_3970_2549)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 12C19.16 12 12 19.16 12 28C12 35.08 16.58 41.06 22.94 43.18C23.74 43.32 24.04 42.84 24.04 42.42C24.04 42.04 24.02 40.78 24.02 39.44C20 40.18 18.96 38.46 18.64 37.56C18.46 37.1 17.68 35.68 17 35.3C16.44 35 15.64 34.26 16.98 34.24C18.24 34.22 19.14 35.4 19.44 35.88C20.88 38.3 23.18 37.62 24.1 37.2C24.24 36.16 24.66 35.46 25.12 35.06C21.56 34.66 17.84 33.28 17.84 27.16C17.84 25.42 18.46 23.98 19.48 22.86C19.32 22.46 18.76 20.82 19.64 18.62C19.64 18.62 20.98 18.2 24.04 20.26C25.32 19.9 26.68 19.72 28.04 19.72C29.4 19.72 30.76 19.9 32.04 20.26C35.1 18.18 36.44 18.62 36.44 18.62C37.32 20.82 36.76 22.46 36.6 22.86C37.62 23.98 38.24 25.4 38.24 27.16C38.24 33.3 34.5 34.66 30.94 35.06C31.52 35.56 32.02 36.52 32.02 38.02C32.02 40.16 32 41.88 32 42.42C32 42.84 32.3 43.34 33.1 43.18C39.42 41.06 44 35.06 44 28C44 19.16 36.84 12 28 12Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_3970_2549">
<rect width="32" height="32" fill="white" transform="translate(12 12)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 188 KiB

@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.22417 1H1.00195V7.22222H7.22417V1Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0015 1H10.7793V7.22222H17.0015V1Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0015 10.7778H10.7793V17H17.0015V10.7778Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.22417 10.7778H1.00195V17H7.22417V10.7778Z" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.22417 1H1.00195V7.22222H7.22417V1Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0015 1H10.7793V7.22222H17.0015V1Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0015 10.7778H10.7793V17H17.0015V10.7778Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.22417 10.7778H1.00195V17H7.22417V10.7778Z" stroke="#FF8544" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 61 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4467_7657)">
<path d="M6.54065 20.7133C6.54065 18.9072 5.07648 17.443 3.27033 17.443C1.46417 17.443 0 18.9072 0 20.7133C0 22.5195 1.46417 23.9837 3.27033 23.9837C5.07648 23.9837 6.54065 22.5195 6.54065 20.7133Z" fill="#17BCAF"/>
<path d="M23.9838 16.3516C23.9838 16.7226 23.958 17.0851 23.9062 17.441C23.3841 21.0997 20.2648 23.9213 16.4767 23.9817C16.4358 23.9817 16.3948 23.9817 16.3538 23.9817H11.994C10.1884 23.9817 8.72378 22.5169 8.72378 20.7113C8.72378 19.8075 9.09048 18.9921 9.68154 18.3988C10.2748 17.8056 11.0902 17.441 11.994 17.441H16.3171C16.8974 17.441 17.4043 17.0031 17.4432 16.4228C17.4841 15.808 17.0118 15.2924 16.4121 15.2622C16.3948 15.2622 16.3754 15.2622 16.3559 15.2622H7.63439C3.78805 15.2622 0.606175 12.419 0.0798164 8.72161C0.0258865 8.36564 0 8.00109 0 7.63222C0 7.26331 0.0258865 6.89875 0.0776594 6.5428C0.60186 2.88203 3.72118 0.062559 7.50708 0C7.54807 0 7.58907 0 7.63006 0H11.9897C13.7975 0 15.2622 1.46475 15.2622 3.27033C15.2622 4.1742 14.8955 4.98961 14.3044 5.58285C13.7112 6.17608 12.8958 6.54065 11.9919 6.54065H7.66889C7.08858 6.54065 6.58163 6.97856 6.5428 7.55888C6.50182 8.17364 6.97424 8.68919 7.57394 8.71945C7.59123 8.71945 7.61061 8.71945 7.63006 8.71945H16.3516C20.1958 8.71945 23.3755 11.5626 23.904 15.26C23.9558 15.616 23.9817 15.9806 23.9817 16.3494L23.9838 16.3516Z" fill="#18FFCD"/>
<path d="M23.984 3.27033C23.984 1.46417 22.5199 0 20.7137 0C18.9076 0 17.4434 1.46417 17.4434 3.27033C17.4434 5.07648 18.9076 6.54065 20.7137 6.54065C22.5199 6.54065 23.984 5.07648 23.984 3.27033Z" fill="#17BCAF"/>
<path d="M20.6855 19.622C20.6855 21.9884 18.8002 23.917 16.4488 23.9817C16.4078 23.9817 16.3669 23.9817 16.3259 23.9817H11.9661C10.1605 23.9817 8.6958 22.517 8.6958 20.7114C8.6958 19.8075 9.06251 18.9921 9.65357 18.3989C10.2469 17.8057 11.0623 17.441 11.9661 17.441H16.2892C16.8694 17.441 17.3764 17.0031 17.4153 16.4229C17.4562 15.8081 16.9838 15.2925 16.3841 15.2623C18.7657 15.2904 20.6898 17.2318 20.6898 19.622H20.6855Z" fill="url(#paint0_linear_4467_7657)"/>
<path d="M3.27237 4.35971C3.27237 1.99326 5.15778 0.0668734 7.50916 0C7.55015 0 7.59107 0 7.63206 0H11.9918C13.7974 0 15.2621 1.46475 15.2621 3.27032C15.2621 4.1742 14.8954 4.98961 14.3044 5.58285C13.7111 6.17608 12.8957 6.54065 11.9918 6.54065H7.66874C7.08847 6.54065 6.58154 6.97856 6.5427 7.55888C6.50172 8.17364 6.97414 8.68919 7.57385 8.71945C5.19229 8.69135 3.26807 6.7499 3.26807 4.35971H3.27237Z" fill="url(#paint1_linear_4467_7657)"/>
</g>
<defs>
<linearGradient id="paint0_linear_4467_7657" x1="8.72017" y1="15.2623" x2="20.7103" y2="15.2623" gradientUnits="userSpaceOnUse">
<stop stop-color="#18C2B0"/>
<stop offset="1" stop-color="#0B5C53"/>
</linearGradient>
<linearGradient id="paint1_linear_4467_7657" x1="15.2601" y1="0" x2="3.26995" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#18C2B0"/>
<stop offset="1" stop-color="#0B5C53"/>
</linearGradient>
<clipPath id="clip0_4467_7657">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+253
View File
@@ -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 });
}
})();
+160 -16
View File
@@ -25,6 +25,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 +34,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';
@@ -58,6 +60,7 @@ import 'react-toastify/dist/ReactToastify.css';
import Drift from "react-driftjs";
import { AppContext } from './context/ContextApi.jsx';
import Navbar from "./components/Navbar.jsx";
import Workflows2 from "./views/Workflows2.jsx";
import AppExplorer from "./views/AppExplorer.jsx";
@@ -186,6 +189,7 @@ const App = (message, props) => {
color: "rgba(255, 255, 255, 0.65)",
minHeight: "100vh",
}}
className='parent-component'
>
<ScrollToTop
getUserNotifications={getUserNotifications}
@@ -211,7 +215,20 @@ const App = (message, props) => {
{ window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
<div style={{ minHeight: 68, maxHeight: 68 }}>
<Header
{/* <Header
notifications={notifications}
setNotifications={setNotifications}
userdata={userdata}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
curpath={curpath}
{...props}
/> */}
<Navbar
notifications={notifications}
setNotifications={setNotifications}
userdata={userdata}
@@ -235,6 +252,7 @@ const App = (message, props) => {
<div style={{ height: 60 }} />
*/}
<Routes>
<Route
exact
path="/login"
@@ -242,7 +260,8 @@ const App = (message, props) => {
<LoginPage
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
register={false}
inregister={false}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
@@ -252,6 +271,63 @@ const App = (message, props) => {
/>
}
/>
<Route
exact
path="/login2"
element={
<LoginPageOld
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={false}
inregister={false}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route
exact
path="/loginsetup"
element={
<LoginPageOld
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={false}
inregister={false}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route
exact
path="/register"
element={
<LoginPage
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
inregister={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route
exact
path="/admin2"
@@ -296,6 +372,7 @@ const App = (message, props) => {
element={
<Admin
isLoggedIn={isLoggedIn}
userdata={userdata}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
@@ -353,6 +430,7 @@ const App = (message, props) => {
}
/>
) : null}
<Route
exact
path="/AdminSetup"
@@ -365,6 +443,7 @@ const App = (message, props) => {
/>
}
/>
<Route
exact
path="/detectionframework"
@@ -557,6 +636,32 @@ const App = (message, props) => {
<Route exact path="/forms/:key/run" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route exact path="/forms/:key" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route
exact
path="/legal/:key"
element={
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
isLoggedIn={isLoggedIn}
{...props}
/>
}
/>
<Route
exact
path="/legal"
element={
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
isLoggedIn={isLoggedIn}
{...props}
/>
}
/>
<Route
exact
path="/docs/:key"
@@ -574,7 +679,6 @@ const App = (message, props) => {
exact
path="/docs"
element={
//navigate(`/docs/about`)
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
@@ -648,18 +752,58 @@ const App = (message, props) => {
/>
}
/>
<Route
exact
path="/dashboards"
element={
<DashboardView
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/dashboard"
element={
<DashboardViews
serverside={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboards"
element={
<DashboardViews
serverside={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboard/:key"
element={
<DashboardViews
serverside={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboards/:key"
element={
<DashboardViews
serverside={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/welcome"
+1 -1
View File
@@ -96,7 +96,7 @@ const AdminNavBar = (props) => {
return (
<Wrapper>
<div style={{ flexDirection: 'column', width: 220, }}>
<nav style={{ padding: '25px 25px 3px 25px', height: "calc(100% - 30px)", fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: '#212121', color: '#9CA3AF' }}>
<nav style={{ padding: '25px 25px 3px 25px', height: isCloud ? "calc(100% - 60px)" : "calc(100% - 30px)" , fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: '#212121', color: '#9CA3AF' }}>
<div style={{ display: 'flex', alignItems: 'center', }}>
<img loading="lazy" src={imageData} alt="Logo" style={{ width: '30px', borderRadius: 8, height: '30px', marginRight: '8px' }} />
<div style={{
+267 -264
View File
@@ -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 (
<div style={{ width: 1030, marginLeft: 20, marginTop:10, paddingRight: 17, paddingLeft: 17}}>
<div style={{ width: 985, display: 'flex', alignItems: 'start', paddingTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
<div style={{ marginLeft: 18 }}>Timeline</div>
</div>
<div style={{ display: "flex", width: "100%", marginTop: 16 }}>
<div>
<div style={{ width: 595, alignItems: 'start', paddingTop: 16, paddingBottom: checked ? 28 : 20, marginRight: 20, fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginLeft: 18, marginRight: 310 }}>Apps</div>
<Switch checked={checked}
onChange={handleChange} /> Category
<div style={{ marginLeft: 16, borderLeft: "1px solid #D9D9D9", width: 10, height: 15 }} />
<Link onClick={() => { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand</Link>
</div>
{expand ? null :
<div style={{ marginTop: 8, display: "flex" }}>
<div style={{ marginLeft: 19, }}>
<Typography style={{ fontSize: 13, color: "#9E9E9E", textAlign: "start" }}>Onboarding</Typography>
<Grid container spacing={2} style={{ marginTop: 1 }} >
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 0.5s ease',
}}><SearchIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 0.5s ease',
}}><MailOutlineIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 0.5s ease',
}}><NewReleasesIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
</Grid>
</div>
<div style={{ borderLeft: "1px solid #494949", justifyContent: "center", alignItems: "center", marginTop: 40, marginLeft: 20, marginRight: 20, height: 40 }}></div>
<div style={{}}>
<Typography style={{ fontSize: 13, color: "#9E9E9E", textAlign: "start" }}>Other</Typography>
<Grid container spacing={2} style={{ marginTop: 1 }} >
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 0.5s ease',
}}><SearchIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 10s ease-in-out',
}}><NewReleasesIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
<Grid item xs={4}>
<div style={{ position: 'relative' }}>
<img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
{checked ?
<div style={{
position: 'absolute',
bottom: 0,
left: '50%',
top: 30,
transform: 'translateX(-50%)',
backgroundColor: '#2f2f2f',
borderRadius: '50%',
height: 24,
width: 24,
transition: 'transform 10s ease-in-out',
}}><MailOutlineIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
null
}
</div>
</Grid>
</Grid>
</div>
</div>}
</div>
<div style={{ width: 595, height: 322, marginTop: 16, paddingTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
<div style={{ marginLeft: 18, textAlign: "start" }}>Workflows</div>
<div style={{textAlign:"center"}}>
<Button style={{ textTransform: "capitalize", background: selectedOption === 'all' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'all' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('all')}>All</Button>
<Button style={{ textTransform: "capitalize", background: selectedOption === 'revisions' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'revisions' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('revisions')}>Revisions</Button>
<Button style={{ textTransform: "capitalize", background: selectedOption === 'run' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'run' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('run')}>Runs</Button>
</div>
<Bar data={data} options={options} style={{ width: 400, marginLeft: 25, padding:20,marginTop: 10 }} />
</div>
</div>
<div style={{ width: 375, height: 440, display: 'flex', alignItems: 'start', paddingTop: '16px', paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
<div style={{ marginLeft: 18 }}>Insights</div>
</div>
</div>
<div style={{ width: 985, paddingTop: 16, marginTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
<div style={{ marginLeft: 18, textAlign: 'start', marginBottom: 16 }}>Sessions Overview</div>
<div style={{ display: "flex", width: "100%", justifyContent: "center" }}>
<div style={{ fontSize: '16px',width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
<div style={{ marginTop: 21, color: "#ffffff", marginLeft:20,fontSize: 16, fontWeight: "bold", }}>
2.8 Hours
</div>
<div style={{ marginTop: 13, marginBottom: 16, marginLeft:20, }}>
Avg. Activity per session
</div>
</div>
<div style={{ marginLeft: 16, fontSize: '16px', width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
<div style={{ marginTop: 21, marginLeft:20, color: "#ffffff", fontSize: 16, fontWeight: "bold", }}>
/usercases/edr to ticket
</div>
<div style={{ marginTop: 13, marginLeft:20, marginBottom: 16, }}>
Last visited page
</div>
</div>
<div style={{ marginLeft: 16, fontSize: '16px', width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
<div style={{ marginTop: 21, marginLeft:20,color: "#ffffff", fontSize: 16, fontWeight: "bold", }}>
/workflow/email management
</div>
<div style={{ marginTop: 13, marginLeft:20, marginBottom: 16, }}>
Most visited page
</div>
</div>
</div>
</div>
</div>
);
// // Options for the chart
// const options = {
// scales: {
// yAxes: [
// {
// ticks: {
// beginAtZero: true
// }
// }
// ]
// },
// };
// return (
// <div style={{ width: 1030, marginLeft: 20, marginTop:10, paddingRight: 17, paddingLeft: 17}}>
// <div style={{ width: 985, display: 'flex', alignItems: 'start', paddingTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
// <div style={{ marginLeft: 18 }}>Timeline</div>
// </div>
// <div style={{ display: "flex", width: "100%", marginTop: 16 }}>
// <div>
// <div style={{ width: 595, alignItems: 'start', paddingTop: 16, paddingBottom: checked ? 28 : 20, marginRight: 20, fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
// <div style={{ display: "flex", alignItems: "center" }}>
// <div style={{ marginLeft: 18, marginRight: 310 }}>Apps</div>
// <Switch checked={checked}
// onChange={handleChange} /> Category
// <div style={{ marginLeft: 16, borderLeft: "1px solid #D9D9D9", width: 10, height: 15 }} />
// <Link onClick={() => { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand</Link>
// </div>
// {expand ? null :
// <div style={{ marginTop: 8, display: "flex" }}>
// <div style={{ marginLeft: 19, }}>
// <Typography style={{ fontSize: 13, color: "#9E9E9E", textAlign: "start" }}>Onboarding</Typography>
// <Grid container spacing={2} style={{ marginTop: 1 }} >
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 0.5s ease',
// }}><SearchIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 0.5s ease',
// }}><MailOutlineIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 0.5s ease',
// }}><NewReleasesIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// </Grid>
// </div>
// <div style={{ borderLeft: "1px solid #494949", justifyContent: "center", alignItems: "center", marginTop: 40, marginLeft: 20, marginRight: 20, height: 40 }}></div>
// <div style={{}}>
// <Typography style={{ fontSize: 13, color: "#9E9E9E", textAlign: "start" }}>Other</Typography>
// <Grid container spacing={2} style={{ marginTop: 1 }} >
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 0.5s ease',
// }}><SearchIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 10s ease-in-out',
// }}><NewReleasesIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// <Grid item xs={4}>
// <div style={{ position: 'relative' }}>
// <img src="/images/adminpage/app1.svg" style={{ margin: 'auto' }} />
// {checked ?
// <div style={{
// position: 'absolute',
// bottom: 0,
// left: '50%',
// top: 30,
// transform: 'translateX(-50%)',
// backgroundColor: '#2f2f2f',
// borderRadius: '50%',
// height: 24,
// width: 24,
// transition: 'transform 10s ease-in-out',
// }}><MailOutlineIcon style={{ width: 16, height: 16, marginTop: 5 }} /></div> :
// null
// }
// </div>
// </Grid>
// </Grid>
// </div>
// </div>}
// </div>
// <div style={{ width: 595, height: 322, marginTop: 16, paddingTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
// <div style={{ marginLeft: 18, textAlign: "start" }}>Workflows</div>
// <div style={{textAlign:"center"}}>
// <Button style={{ textTransform: "capitalize", background: selectedOption === 'all' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'all' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('all')}>All</Button>
// <Button style={{ textTransform: "capitalize", background: selectedOption === 'revisions' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'revisions' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('revisions')}>Revisions</Button>
// <Button style={{ textTransform: "capitalize", background: selectedOption === 'run' ? "#494949" : "#1A1A1A", borderRadius: selectedOption === 'run' ? 15 : null, color: "#FFFFFF" }} onClick={() => handleOptionChange('run')}>Runs</Button>
// </div>
// <Bar data={data} options={options} style={{ width: 400, marginLeft: 25, padding:20,marginTop: 10 }} />
// </div>
// </div>
// <div style={{ width: 375, height: 440, display: 'flex', alignItems: 'start', paddingTop: '16px', paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
// <div style={{ marginLeft: 18 }}>Insights</div>
// </div>
// </div>
// <div style={{ width: 985, paddingTop: 16, marginTop: 16, paddingBottom: '48px', fontSize: '16px', color: '#ffffff', backgroundColor: '#1A1A1A', borderRadius: '16px', }}>
// <div style={{ marginLeft: 18, textAlign: 'start', marginBottom: 16 }}>Sessions Overview</div>
// <div style={{ display: "flex", width: "100%", justifyContent: "center" }}>
// <div style={{ fontSize: '16px',width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
// <div style={{ marginTop: 21, color: "#ffffff", marginLeft:20,fontSize: 16, fontWeight: "bold", }}>
// 2.8 Hours
// </div>
// <div style={{ marginTop: 13, marginBottom: 16, marginLeft:20, }}>
// Avg. Activity per session
// </div>
// </div>
// <div style={{ marginLeft: 16, fontSize: '16px', width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
// <div style={{ marginTop: 21, marginLeft:20, color: "#ffffff", fontSize: 16, fontWeight: "bold", }}>
// /usercases/edr to ticket
// </div>
// <div style={{ marginTop: 13, marginLeft:20, marginBottom: 16, }}>
// Last visited page
// </div>
// </div>
// <div style={{ marginLeft: 16, fontSize: '16px', width: "30%", borderRadius: 8, background: '#212121', color: '#9CA3AF' }}>
// <div style={{ marginTop: 21, marginLeft:20,color: "#ffffff", fontSize: 16, fontWeight: "bold", }}>
// /workflow/email management
// </div>
// <div style={{ marginTop: 13, marginLeft:20, marginBottom: 16, }}>
// Most visited page
// </div>
// </div>
// </div>
// </div>
// </div>
// );
};
export default AnalyticsTab;
+23 -16
View File
@@ -842,7 +842,7 @@ const AppAuthTab = memo((props) => {
hitsPerPage={5}
searchQuery={searchQuery}
globalUrl={globalUrl}
isCloud={isCloud}
isCloud={isCloud !== true}
userdata={userdata}
getAppAuthentication={getAppAuthentication}
/>
@@ -874,14 +874,18 @@ const AppAuthTab = memo((props) => {
</a>
</div>
</div>
<Button
style={{ color: '#1a1a1a', textTransform: 'none', backgroundColor: "#FF8444", marginLeft:"auto", borderRadius: 4,fontSize: 16, minWidth: 162, height: 40, boxShadow:'none', }}
variant="contained"
color="primary"
onClick={() => setShowAppModal(true)}
>
Add App Auth
</Button>
{isCloud ?
<Button
style={{ color: '#1a1a1a', textTransform: 'none', backgroundColor: "#FF8444", marginLeft:"auto", borderRadius: 4,fontSize: 16, minWidth: 162, height: 40, boxShadow:'none', }}
variant="contained"
color="primary"
disabled={!isCloud}
onClick={() => setShowAppModal(true)}
>
Add App Auth
</Button>
: null}
</div>
{/* <Divider
style={{
@@ -1216,6 +1220,8 @@ const AppAuthTab = memo((props) => {
{editAuthenticationModal}
{authenticationView}
<div style={{marginTop: 50, }}>
{/*
<div style={{ marginTop: 150, marginBottom: 20 }}>
<h2 style={{ color: "#FFFFFF" }}>App Authentication Groups</h2>
<span style={{ marginLeft: 0 }}>
@@ -1414,13 +1420,14 @@ const AppAuthTab = memo((props) => {
);
}
)}
</List>
</div>
</div>
</div>
</div>
</div>
</List>
</div>
</div>
*/}
</div>
</div>
</div>
);
});
+22 -21
View File
@@ -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)
@@ -59,6 +58,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 +256,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 = () => {
@@ -416,7 +417,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 3 }}>
<div style={{ display: "flex", gap: '16px' }}>
<div style={{ display: "flex", gap: 16, }}>
<AppCreateButton
text="Upload OpenAPI or Swagger"
func={() => {
+1 -1
View File
@@ -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
+16 -16
View File
@@ -116,7 +116,7 @@ const AppGrid = (props) => {
})
.then((response) => response.json())
.then((response) => {
if (response.success === true) {
if (response?.success === true) {
setFormMessage(response.reason);
//toast("Thanks for submitting!")
} else {
@@ -307,7 +307,7 @@ const AppGrid = (props) => {
})
.then(response => response.json())
.then(responseJson => {
if (responseJson.success) {
if (responseJson?.success) {
setUserdata(responseJson);
setAllActivatedAppIds(responseJson.active_apps)
setIsLoggedIn(true);
@@ -350,7 +350,7 @@ const AppGrid = (props) => {
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson?.success === false) {
toast.error(responseJson.reason);
} else {
//toast.success(`App ${type}d Successfully!`);
@@ -414,7 +414,7 @@ const AppGrid = (props) => {
scrollbarColor: "#494949 #2f2f2f",
}}
>
{hits.map((data, index) => {
{hits?.map((data, index) => {
const appUrl =
isCloud === true ?
`/apps/${data.objectID}`
@@ -556,7 +556,7 @@ const AppGrid = (props) => {
}}
>
<span>
{data.tags.slice(0, 1).map((tag, tagIndex) => (
{data?.tags?.slice(0, 1)?.map((tag, tagIndex) => (
<span key={tagIndex}>
{normalizedString(tag)}
{tagIndex < 1 ? ", " : ""}
@@ -569,7 +569,7 @@ const AppGrid = (props) => {
) : (
<div style={{ width: 230, textOverflow: "ellipsis", overflow: 'hidden', whiteSpace: 'nowrap', }}>
{data.tags &&
data.tags.map((tag, tagIndex) => (
data?.tags?.map((tag, tagIndex) => (
<span key={tagIndex}>
{normalizedString(tag)}
{tagIndex < data.tags.length - 1 ? ", " : ""}
@@ -760,7 +760,7 @@ const AppGrid = (props) => {
};
const transformRefinementListItems = items =>
items.map(item => ({
items?.map(item => ({
...item,
label: item.label === 'true' ? 'App Editor' : 'Python',
}));
@@ -1103,7 +1103,7 @@ const AppGrid = (props) => {
}
});
const categoryArray = Object.keys(categoryCountMap).map((category) => ({
const categoryArray = Object.keys(categoryCountMap)?.map((category) => ({
category,
count: categoryCountMap[category],
}));
@@ -1169,7 +1169,7 @@ const AppGrid = (props) => {
{!isLoading && (
<Collapse in={isCategoreListExpanded}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', width: '100%' }}>
{topCategories.map((data, index) => (
{topCategories?.map((data, index) => (
<Button
key={data.category}
style={{
@@ -1247,7 +1247,7 @@ const AppGrid = (props) => {
});
}
const tagArray = Object.keys(tagCountMap).map((tag) => ({
const tagArray = Object.keys(tagCountMap)?.map((tag) => ({
tag,
count: tagCountMap[tag],
}));
@@ -1308,7 +1308,7 @@ const AppGrid = (props) => {
</Button>
<Collapse in={isActionLabelExpanded}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', width: '100%', }}>
{topTags && topTags.length > 0 && topTags.map((data, index) => (
{topTags && topTags.length > 0 && topTags?.map((data, index) => (
<Button
key={index}
onClick={() => handleCheckboxChange(index)}
@@ -1419,7 +1419,7 @@ const AppGrid = (props) => {
<Collapse in={isCreatedWithExpanded}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', width: '100%' }}>
{AppCreatedWithOptions.map((data, index) => (
{AppCreatedWithOptions?.map((data, index) => (
<Button
style={{
display: "inline-flex",
@@ -1689,7 +1689,7 @@ const AppGrid = (props) => {
maxHeight: 570,
}}
>
{filteredUserAppdata.map((data, index) => {
{filteredUserAppdata?.map((data, index) => {
const isMouseOverOnCloudIcon = false;
const xs = 12;
const rowHandler = 12;
@@ -1847,8 +1847,8 @@ const AppGrid = (props) => {
<div style={{minWidth: 120, overflow: "hidden", }}>
{data.generated !== true ?
<div>
{data.tags &&
data.tags.slice(0,2).map((tag, tagIndex) => (
{data?.tags &&
data?.tags?.slice(0,2)?.map((tag, tagIndex) => (
<span key={tagIndex}>
{normalizedString(tag)}
{tagIndex < data.tags.length - 1 ? ", " : ""}
@@ -1888,7 +1888,7 @@ const AppGrid = (props) => {
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson?.success === false) {
toast.error(responseJson.reason);
} else {
toast.success("App Deactivated Successfully. Reload UI to see updated changes.")
+64 -18
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import {
@@ -29,8 +29,15 @@ import theme 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";
const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
const searchClient = algoliasearch(
"JNSS5CFDZZ",
"db08e40265e2941b9a7d8f644b6e5240"
);;
const AppModal = ({ open, onClose, app, globalUrl, getApps}) => {
const [frameworkData, setFrameworkData] = useState({})
const [userdata, setUserdata] = useState({})
@@ -79,8 +86,7 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
});
}, [app]);
const getFramework = () => {
const getFramework = () => {
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
@@ -295,8 +301,6 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
})
}
useEffect(() => {
setUsecaseLoading(true)
getAvailableWorkflows()
@@ -329,6 +333,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?");
@@ -720,18 +725,7 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
textAlign: "start",
flex: 1,
}}>
<Typography
variant="h6"
sx={{
fontFamily: theme?.typography?.fontFamily,
fontSize: '24px',
fontWeight: 600,
mb: 0.3,
color: '#fff'
}}
>
20
</Typography>
<WorkflowCard app={app}/>
<Typography
variant="body2"
sx={{
@@ -930,3 +924,55 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
};
export default AppModal;
const WorkflowCard = memo(({ app }) => {
const [name, setName] = useState("");
const [relatedWorkflows, setRelatedWorkflows] = useState(0);
const WorkflowHits = ({ hits }) => {
useEffect(() => {
setRelatedWorkflows(hits?.length || 0);
}, [hits]);
return null;
};
const CustomWorkflowHits = connectHits(WorkflowHits);
const SearchBox = ({ refine, currentRefinement }) => {
useEffect(() => {
if (name.length > 0 && currentRefinement !== name) {
refine(name?.split(" ")[0]);
}
}, [name, refine, currentRefinement]);
return null;
};
const CustomSearchBox = connectSearchBox(SearchBox);
useEffect(() => {
if (app?.name && app.name.trim().length > 0 && name !== app.name) {
const formattedName = app.name
.charAt(0)
.toUpperCase() + app.name.substring(1)
.replaceAll("_", " ");
setName(formattedName);
}
}, [app?.name]);
return (
<Typography variant="h4">
{name.length > 0 ? (
<InstantSearch key={name} searchClient={searchClient} indexName="workflows">
<CustomSearchBox defaultRefinement={name?.split(" ")[0]}/>
<CustomWorkflowHits />
{relatedWorkflows}
</InstantSearch>
) : (
relatedWorkflows
)}
</Typography>
);
});
+2 -2
View File
@@ -88,7 +88,7 @@ const AppSearchButtons = (props) => {
const foundApp = findSpecificApp(appFramework, appType)
if (foundApp === undefined || foundApp === null) {
console.log("AppSearchButtons: App not found in appFramework: " + appType)
//console.log("AppSearchButtons: App not found in appFramework: " + appType)
return null
}
@@ -179,7 +179,7 @@ const AppSearchButtons = (props) => {
return (
<Grid item xs={xsValue} style={{ alignItems: "center", marginTop: 5, }}
<Grid item xs={xsValue} style={{ alignItems: "center", marginTop: 5, maxWidth: "50%", minWidth: "50%" }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
+53 -41
View File
@@ -77,6 +77,8 @@ const Billing = memo((props) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1);
const [deleteAlertVerification, setDeleteAlertVerification] = useState(false);
const [isScale, setIsScale] = useState(false);
useEffect(() => {
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100;
@@ -176,11 +178,10 @@ const Billing = memo((props) => {
padding: 20,
// maxWidth: 400,
width: 340,
height: 480,
height: 'auto',
// width: "100%",
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette?.borderRadius * 2,
border: "1px solid rgba(255,255,255,0.3)",
backgroundColor: "#1e1e1e",
borderRadius: 10,
marginRight: 10,
marginTop: 15,
}
@@ -361,7 +362,7 @@ const Billing = memo((props) => {
var showSupport = false
if (subscription.name.includes("default")) {
top_text = "Custom Contract"
newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
// newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
showSupport = true
}
@@ -377,16 +378,17 @@ const Billing = memo((props) => {
if (subscription.name.includes("Scale")) {
top_text = "Scale access"
setIsScale(true)
}
if (highlight === true) {
// Add an "Upgrade now" button
newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
// newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
}
if (hovered) {
newPaperstyle.backgroundColor = "#2b2b2b"
}
// if (hovered) {
// newPaperstyle.backgroundColor = "#2b2b2b"
// }
const handleClickOpen = () => {
setOpenChangeEmailBox(true);
@@ -556,6 +558,7 @@ const Billing = memo((props) => {
</div>
</DialogContent>
</Dialog>
<Button style={{ backgroundColor: '#2F2F2F', color: "white", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', width: 144, height: 40 }}>Current Plan</Button>
<div style={{ display: "flex", width: 340 }}>
{top_text === "Base Cloud Access" && userdata.has_card_available === true ?
<Chip
@@ -659,8 +662,8 @@ const Billing = memo((props) => {
<TextField
value={feature.split("Worker License: ")[1]}
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
borderRadius: theme.palette?.textFieldStyle.borderRadius,
}}
id={fieldId}
onClick={() => { }}
@@ -857,6 +860,7 @@ const Billing = memo((props) => {
"Add Card Details"
}
</Button>
{userdata.has_card_available === true ?
<Button
fullWidth
@@ -885,7 +889,8 @@ const Billing = memo((props) => {
: null}
</span>
: null}
: null}
{showSupport ?
<Button variant="outlined" color="primary" style={{ marginTop: 20, marginBottom: 10, }} onClick={() => {
if (window.drift !== undefined) {
@@ -1201,12 +1206,13 @@ const Billing = memo((props) => {
ReactGA.event({
category: "Billing",
action: "Buy_consultation_hours",
});
})
}
const url = userdata.licensed === true
? "https://book.stripe.com/6oEeY23fw2T84M06oq"
: "https://book.stripe.com/00g6rw3fw9hwguI289";
window.open(url, "_blank");
? "https://buy.stripe.com/aEU3fk17o0L092g5kt"
: "https://buy.stripe.com/aEU3fk17o0L092g5kt"
window.open(url, "_blank")
}}
>
Book Professional Service Hours
@@ -2013,7 +2019,33 @@ const Billing = memo((props) => {
</Typography>
: null}
<div style={{ display: "flex", width: clickedFromOrgTab ? "100%" : "auto", overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'thin', scrollbarColor: '#494949 #2f2f2f', height: isChildOrg ? 0 : "100%", maxWidth: 860, marginTop: 20}} >
<div style={{ display: "flex", width: clickedFromOrgTab ? "100%" : "auto", overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'thin', scrollbarColor: '#494949 #2f2f2f', height: isChildOrg ? 0 : "100%", marginTop: 20}} >
<div style={{ display: "flex", flexDirection: "column", width: "100%", }}>
{isCloud &&
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 &&
!isChildOrg ?
selectedOrganization.subscriptions
.reverse()
.map((sub, index) => {
return (
<SubscriptionObject
index={index + 1}
globalUrl={globalUrl}
userdata={userdata}
serverside={serverside}
billingInfo={billingInfo}
stripeKey={stripeKey}
selectedOrganization={selectedOrganization}
subscription={sub}
highlight={true}
/>
)
})
: null}
<div style={{ display: "flex", flexDirection: "row", width: "100%", marginTop: 20, marginBottom: 20, maxWidth: 860, }}>
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null :
<LicencePopup
serverside={serverside}
@@ -2026,6 +2058,7 @@ const Billing = memo((props) => {
isCloud={isCloud}
userdata={userdata}
stripeKey={stripeKey}
isScale={isScale}
{...props}
/>
: !isCloud ?
@@ -2061,6 +2094,7 @@ const Billing = memo((props) => {
isCloud={isCloud}
userdata={userdata}
stripeKey={stripeKey}
isScale={isScale}
/>
{/* <SubscriptionObject
@@ -2084,30 +2118,9 @@ const Billing = memo((props) => {
/> */}
</span>
: null}
</div>
</div>
{isCloud &&
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 &&
!isChildOrg ?
selectedOrganization.subscriptions
.reverse()
.map((sub, index) => {
return (
<SubscriptionObject
index={index + 1}
globalUrl={globalUrl}
userdata={userdata}
serverside={serverside}
billingInfo={billingInfo}
stripeKey={stripeKey}
selectedOrganization={selectedOrganization}
subscription={sub}
highlight={true}
/>
)
})
: null}
{/*
<Grid item key={index} xs={12/selectedOrganization.subscriptions.length}>
<Card
@@ -2153,7 +2166,6 @@ const Billing = memo((props) => {
*/}
</div>
{/*isCloud &&
selectedOrganization.partner_info !== undefined &&
selectedOrganization.partner_info.reseller === true ? (
+289 -61
View File
@@ -24,11 +24,14 @@ import {
Checkbox,
MenuItem,
DialogContent,
FormControl,
Select,
} from "@mui/material";
import {
Link as LinkIcon,
AutoFixHigh as AutoFixHighIcon,
AutoFixNormal as AutoFixNormalIcon,
Edit as EditIcon,
FileCopy as FileCopyIcon,
SelectAll as SelectAllIcon,
@@ -50,8 +53,8 @@ import {
Business as BusinessIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
CheckBox,
Key,
Clear as ClearIcon,
Add as AddIcon,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
@@ -91,15 +94,40 @@ const CacheView = memo((props) => {
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
const [selectedCacheKey, setSelectedCacheKey] = 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 [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
const [showFileCategoryPopup, setShowFileCategoryPopup] = React.useState(false);
const [selectedFiles, setSelectedFiles] = useState([]);
useEffect(() => {
if(orgId?.length >0){
listOrgCache(orgId);
if (orgId?.length > 0) {
listOrgCache(orgId, selectedCategory)
}
}, [orgId]);
}, [orgId])
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
fileCategories.push(event.target.value);
setSelectedCategory(event.target.value);
setRenderTextBox(false);
}
if (event.key === 'Escape'){ // not working for some reasons
console.log('escape pressed')
setRenderTextBox(false);
}
}
const listOrgCache = (orgId) => {
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
const listOrgCache = (orgId, category) => {
const url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache${category !== undefined ? `?category=${category.replaceAll(" ", "%20")}` : ""}`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -119,6 +147,19 @@ const CacheView = memo((props) => {
if (responseJson.success === true) {
setListCache(responseJson.keys);
setCachedLoaded(true);
if (fileCategories.length === 1 && fileCategories[0] === "default") {
var newcategories = ["default"]
for (var key in responseJson.keys) {
if (responseJson.keys[key].category !== undefined && responseJson.keys[key].category !== null && responseJson.keys[key].category !== "" && !fileCategories.includes(responseJson.keys[key].category)) {
newcategories.push(responseJson.keys[key].category);
}
}
console.log("CATEGORIES: ", newcategories)
setFileCategories(newcategories)
}
}
if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
@@ -132,15 +173,12 @@ const CacheView = memo((props) => {
const deleteCache = (orgId, key) => {
//toast("Attempting to delete Cache");
// method: "DELETE",
const method = "POST"
//const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/${key}`
const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache`
const parsed = {
"org_id": orgId,
"key": key,
"category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory,
}
fetch(url, {
@@ -155,7 +193,7 @@ const CacheView = memo((props) => {
if (response.status === 200) {
toast("Successfully deleted Cache");
setTimeout(() => {
listOrgCache(orgId);
listOrgCache(orgId, selectedCategory)
}, 1000);
} else {
toast("Failed deleting Cache. Does it still exist?");
@@ -167,7 +205,12 @@ const CacheView = memo((props) => {
};
const editOrgCache = (orgId) => {
const cache = { key: dataValue.key , value: value };
const cache = {
key: dataValue.key,
value: value,
category: selectedCategory,
}
setCacheInput([cache]);
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
@@ -191,7 +234,7 @@ const CacheView = memo((props) => {
.then((responseJson) => {
setAddCache(responseJson);
toast("Cache Edited Successfully!");
listOrgCache(orgId);
listOrgCache(orgId, selectedCategory);
setModalOpen(false);
})
.catch((error) => {
@@ -200,9 +243,13 @@ const CacheView = memo((props) => {
};
const addOrgCache = (orgId) => {
const cache = { key: key, value: value };
const cache = {
key: key,
value: value,
category: selectedCategory,
}
setCacheInput([cache]);
console.log("cache input:", cacheInput)
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
@@ -224,8 +271,8 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New key Added Successfully!");
listOrgCache(orgId);
toast("New key added Successfully!");
listOrgCache(orgId, selectedCategory);
setModalOpen(false);
})
.catch((error) => {
@@ -243,7 +290,9 @@ const CacheView = memo((props) => {
setValue(JSON.stringify(parsedjson, null, 2))
} catch (e) {
console.log("Error parsing JSON: ", e)
//return JSON.stringify(inputvalue);
toast.info("Invalid JSON.", {
autoClose: 1500,
})
}
}
@@ -306,13 +355,14 @@ const CacheView = memo((props) => {
>
<DialogTitle>
<span style={{ color: "white" }}>
{ editCache ? "Edit Key" : "Add Key" }
{ editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`}
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: "#212121", }}>
Key
<TextField
color="primary"
disabled={editCache}
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor }}
autoFocus
InputProps={{
@@ -345,7 +395,7 @@ const CacheView = memo((props) => {
autoFixJson(value)
}}
>
<AutoFixHighIcon />
<AutoFixNormalIcon />
</IconButton>
</Tooltip>
</div>
@@ -436,46 +486,49 @@ const CacheView = memo((props) => {
const changeDistribution = (id, selectedSubOrg) => {
editFileConfig(id, [...new Set(selectedSubOrg)])
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
}
const editFileConfig = (id, selectedSubOrg, cacheKey) => {
const data = {
Key: id,
action: "suborg_distribute",
selected_suborgs: selectedSubOrg,
}
console.log("data: ", data);
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
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) => {
if (responseJson["success"] === false) {
toast("Failed overwriting datastore");
} else {
toast("Successfully updated datastore!");
setTimeout(() => {
listOrgCache(orgId);
setShowDistributionPopup(false);
}, 1000);
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
const editFileConfig = (id, selectedSubOrg, category) => {
const data = {
Key: id,
action: "suborg_distribute",
selected_suborgs: selectedSubOrg,
category: category === undefined || category === "" || category === "default" ? "" : category,
}
console.log("data: ", data);
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
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) => {
if (responseJson["success"] === false) {
toast("Failed overwriting datastore");
} else {
toast("Successfully updated datastore!");
setTimeout(() => {
listOrgCache(orgId, selectedCategory);
setShowDistributionPopup(false);
}, 1000);
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
@@ -579,7 +632,7 @@ const CacheView = memo((props) => {
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' }}>
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
<h2 style={{ display: isSelectedDataStore?null: "inline" }}>Shuffle Datastore</h2>
<h2 style={{ display: isSelectedDataStore?null: "inline" }}>Shuffle Datastore {selectedCategory === "" || selectedCategory === "default" ? "" : `- Category '${selectedCategory}'`}</h2>
<span style={{ marginLeft: isSelectedDataStore?null:25, color:isSelectedDataStore?"#9E9E9E":null}}>
Datastore is a permanent key-value database for storing data that can be used cross-workflow. <br/>You can store anything from lists of IPs to complex configurations.&nbsp;
<a
@@ -609,10 +662,175 @@ const CacheView = memo((props) => {
style={{ marginLeft: 16, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?4:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
variant="contained"
color="primary"
onClick={() => listOrgCache(orgId)}
onClick={() => listOrgCache(orgId,selectedCategory)}
>
<CachedIcon />
</Button>
{fileCategories !== undefined &&
fileCategories !== null &&
fileCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
<Select
labelId="input-namespace-select-label"
id="input-namespace-select-id"
style={{
color: "white",
minWidth: 122,
maxWidth: 122,
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
}
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 (
<MenuItem
key={index}
value={data}
style={{ color: "white" }}
>
{data.replaceAll("_", " ")}
</MenuItem>
);
})}
</Select>
<Dialog
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
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,
},
},
}}
open={showFileCategoryPopup}
onClose={() => {
setShowFileCategoryPopup(false)
}}
>
<DialogTitle>File Categories</DialogTitle>
<DialogContent>
Please note that your selected files ({selectedFileId?.length}) will be moved to the <kbd>{updateToThisCategory}</kbd> category.
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setShowFileCategoryPopup(false)
}}
style={{fontSize: 16, textTransform: 'none'}}
>
Close
</Button>
<Button
onClick={() => {
//handleUpdateFileCategory(updateToThisCategory)
toast.error("Not implemented.")
}}
style={{fontSize: 16, textTransform: 'none', color: "#1a1a1a", backgroundColor: "#ff8544"}}
>
Update
</Button>
</DialogActions>
</Dialog>
</FormControl>
) : null}
<div style={{display: "inline-flex", position:"relative", top: 8}}>
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15, height: 35, borderRadius: 4, backgroundColor: "#494949", textTransform: 'none', fontSize: 16, color: "#f1f1f1" }}
color="primary"
onClick={() => {
setRenderTextBox(false);
console.log(" close clicked")
}}
>
<ClearIcon/>
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15, width: 169, height: 35, borderRadius: 4, backgroundColor: "#494949", textTransform: 'none', fontSize: 16, color: "#f1f1f1" }}
color="primary"
onClick={() => {
setRenderTextBox(true);
}}
>
<AddIcon/>
Category (beta)
</Button>
</Tooltip>
}
{renderTextBox && <TextField
onKeyPress={(event)=>{
handleKeyDown(event);
if(event.key === 'Enter' && selectedFileId.length > 0){
//setShowFileCategoryPopup(true)
setUpdateToThisCategory(event.target.value)
}
}}
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
/>}</div>
{isSelectedDataStore? null :<Divider
style={{
marginTop: 20,
@@ -704,6 +922,16 @@ const CacheView = memo((props) => {
</ListItem>
): listCache?.map((data, index) => {
var category = selectedCategory
if (selectedCategory === "default") {
category = ""
}
if (data?.category === undefined && category === "") {
} else if (data?.category !== category) {
return null
}
var bgColor = isSelectedDataStore? "#212121":"#27292d";
if (index % 2 === 0) {
bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023";
+75 -72
View File
@@ -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 (
<Bar
data={timelineData}
options={barOptions}
height={inputHeight}
getElementAtEvent={(elements) => {
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 (
// <Bar
// data={timelineData}
// options={barOptions}
// height={inputHeight}
// getElementAtEvent={(elements) => {
// if (elements && elements.length > 0) {
// //toast("Click event")
// console.log("Clicked: ", elements)
// }
// }}
// />
// )
}
export default DashboardBarchart;
+4 -4
View File
@@ -1,8 +1,8 @@
import React, { useEffect, useState, useContext } from 'react';
import OrgHeaderexpanded from "./OrgHeaderexpandedNew.jsx";
import OrgHeader from './OrgHeaderNew.jsx';
import OrgHeaderexpanded from "../components/OrgHeaderexpandedNew.jsx";
import OrgHeader from '../components/OrgHeaderNew.jsx';
import { toast } from "react-toastify";
import CloudSyncTab from './CloudSyncTab.jsx';
import CloudSyncTab from '../components/CloudSyncTab.jsx';
import {
FileCopy as FileCopyIcon,
} from "@mui/icons-material";
@@ -443,4 +443,4 @@ If you're interested, please let me know a time that works for you, or set up a
)
}
export default EditOrgTab;
export default EditOrgTab;
+116 -13
View File
@@ -88,6 +88,8 @@ const EditWorkflow = (props) => {
const [inputMarkdown, setInputMarkdown] = React.useState(workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null ? workflow?.form_control?.input_markdown : "")
const [scrollDone, setScrollDone] = React.useState(false)
const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow?.form_control?.output_yields !== undefined && workflow?.form_control?.output_yields !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.output_yields)) : [])
const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : [])
const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth)
const classes = useStyles();
@@ -189,6 +191,10 @@ const EditWorkflow = (props) => {
var upload = "";
var total_count = 0
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
return (
<Drawer
anchor={"right"}
@@ -296,6 +302,7 @@ const EditWorkflow = (props) => {
<Button
variant="contained"
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true}
onClick={() => {
setSubmitLoading(true)
@@ -323,6 +330,7 @@ const EditWorkflow = (props) => {
innerWorkflow.form_control.input_markdown = inputMarkdown
innerWorkflow.form_control.output_yields = selectedYieldActions
innerWorkflow.form_control.form_width = formWidth
innerWorkflow.form_control.cleanup_actions = selectedCleanupActions
innerWorkflow.name = name
innerWorkflow.description = description
@@ -399,6 +407,7 @@ const EditWorkflow = (props) => {
label="Name"
autoFocus
fullWidth
id="Enter-Workflow-Name"
/>
<div style={{ display: "flex", marginTop: 10, }}>
@@ -568,14 +577,22 @@ const EditWorkflow = (props) => {
<Divider id="mssp_control" style={{ marginTop: 20, marginBottom: 20, }} />
<Typography variant="h4" style={{ marginTop: 50, }}>
MSSP & Distribution controls
Multi-Tenancy, Backups & Security
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 30, marginBottom: 10, }}>
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. (<b>late beta</b> - contact support@shuffler.io if you want a demo. Please try it!)
Control mechanisms for multi-tenancy, backups, and security.
</Typography>
{userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ?
<Typography variant="h6" style={{ marginTop: 50, }}>
Multi-Tenant Workflows
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, marginBottom: 10, }}>
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? <a href="https://shuffler.io/contact" target="_blank" style={{color: "#f85a3e", textDecoration: "none",}}>Contact us for a demo</a>
</Typography>
{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 === "" ?
<Typography variant="body2" style={{ marginTop: 10, color: "rgba(255,255,255,0.7)" }}>
@@ -593,6 +610,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") {
@@ -619,6 +637,7 @@ const EditWorkflow = (props) => {
All
</MenuItem>
{userdata.orgs.map((data, index) => {
var skipOrg = false;
if (data.creator_org !== undefined && data.creator_org !== null && data.creator_org === userdata.active_org.id) {
// Finds the parent org
@@ -626,6 +645,8 @@ const EditWorkflow = (props) => {
return null
}
const correctRegion = data?.region_url === userdata?.region_url
const imagesize = 22
const imageStyle = {
width: imagesize,
@@ -659,7 +680,11 @@ const EditWorkflow = (props) => {
return (
<MenuItem key={index} value={data.id}>
<MenuItem
key={index}
value={data.id}
disabled={isCloud && !correctRegion}
>
<Checkbox checked={innerWorkflow.suborg_distribution !== undefined && innerWorkflow.suborg_distribution !== null && innerWorkflow.suborg_distribution.includes(data.id)} />
{image}{" "}
<span style={{ marginLeft: 8 }}>
@@ -678,7 +703,7 @@ const EditWorkflow = (props) => {
}
<Typography variant="body1" style={{ marginTop: 75, }}>
<Typography variant="h6" style={{ marginTop: 75, }}>
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
@@ -824,6 +849,60 @@ const EditWorkflow = (props) => {
</Grid>
</Grid>
<div id="cleanup">
<Typography variant="h6" style={{ marginTop: 50, }}>
Result cleanup ({selectedCleanupActions.length === 0 ? "No cleanup yet" : selectedCleanupActions.length === 1 ? "Cleaning up 1 node" : `Cleaning up ${selectedCleanupActions.length} nodes`})
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
<b>Beta Feature</b>: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
</Typography>
<FormControl style={{ marginTop: 15, }}>
<Select
defaultValue=""
id="result-cleanup-control"
label="Cleaned Up nodes"
multiple
fullWidth
style={{ width: 500, }}
value={selectedCleanupActions === [] ? ["none"] : selectedCleanupActions}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
if (event.target.value.length > 0) {
if (event.target.value.includes("none")) {
setSelectedCleanupActions([])
return
}
}
const newvalue = event?.target?.value
if (newvalue === undefined || newvalue === null) {
} else {
setSelectedCleanupActions(newvalue)
}
}}
>
<MenuItem value="none">
<em>None</em>
</MenuItem>
{workflow?.actions?.map((action, actionIndex) => {
return (
<MenuItem
key={actionIndex}
value={action.id}
>
<Tooltip title={action.app_name} key={actionIndex}>
<img src={action.large_image !== undefined && action.large_image !== null && action.large_image.length > 0 ? action.large_image : theme.palette.defaultImage} style={{ width: 20, height: 20, marginRight: 10, }} />
</Tooltip>
{action.label}
</MenuItem>
)
})}
</Select>
</FormControl>
</div>
<Divider style={{ marginTop: 20, marginBottom: 20, }} />
@@ -1023,11 +1102,11 @@ const EditWorkflow = (props) => {
<div id="output_control">
<Typography variant="h6" style={{ marginTop: 50, }}>
Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`})
Form Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`})
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
When running this workflow, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases.
When running this workflow as a form, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases.
</Typography>
<FormControl style={{ marginTop: 15, }}>
@@ -1076,14 +1155,26 @@ const EditWorkflow = (props) => {
</FormControl>
</div>
<Divider style={{marginTop: 75, marginBottom: 75, }}/>
<Typography variant="h4" style={{ marginTop: 100, }}>
<Typography variant="h4" style={{ }}>
Publishing
<Chip
style={{ marginLeft: 20, marginTop: 10, }}
color={workflow?.public === true ? "primary" : "secondary"}
variant={workflow?.public === true ? "default" : "outlined"}
label={workflow?.public === true ? "Public" : "NOT Public"}
/>
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, }}>
Publishing is related to making the workflow itself public. When publishing a workflow, all the details (except sensitive info) become available to everyone. The details below will help a user understand this better. When a workflow is published, you keep the original, and a copy enters the workflow search, and is associated with your <a href="/creators" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">creator account</a>, if you have one. You can always unpublish the workflow after. When ready to publish, click the three dots next to a workflow on the main workflow screen. After publishing, you can find it in the Shuffle search engine.
Publishing is related to making this workflow itself public. When publishing a workflow, all the details (except sensitive info) become available to anyone. The fields below will help a user and Shuffle's system understand your workflow better. When a workflow is published, you keep the original, and a copy enters the Shuffle workflow search, and is associated with your <a href="/creators" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">creator</a> or partner account, if you have one. You can always unpublish the workflow after. When ready to publish, click the three dots next to a workflow on the main workflow page.
You can always unpublish a workflow after.
</Typography>
<LocalizationProvider style={{marginLeft: 0, }} dateAdapter={AdapterDayjs}>
<DatePicker
sx={{
@@ -1113,9 +1204,21 @@ const EditWorkflow = (props) => {
setInnerWorkflow(innerWorkflow)
}}
>
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
<Tooltip title="Agentic workflows takes an input based on input questions (forms) and performs actions based on it by itself, using Large Action Models & Singul">
<FormControlLabel value="agentic" control={<Radio />} label="Agentic" />
</Tooltip>
<Tooltip title="Trigger workflows are typically running a schedule to get some data, doing some deduplication before sending it to a subflow or standalone workflow.">
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
</Tooltip>
<Tooltip title="Subflow workflows are typically used to subprocess some data, and in some cases return the result to the parent workflow.">
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
</Tooltip>
<Tooltip title="Standalone is default. This has no impact on Shuffle as a system.">
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
</Tooltip>
</RadioGroup>
</FormControl>
+22 -13
View File
@@ -421,7 +421,7 @@ const EnvironmentTab = memo((props) => {
-e AUTH="${auth}" \\
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${environment.org_id}" \\
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:nightly" \\
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\
-e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
@@ -1064,17 +1064,26 @@ const EnvironmentTab = memo((props) => {
}}
style={{minWidth:120, display: "table-cell",}}
primary={
<Tooltip title={environment.Type !== "cloud"
? environment.running_ip === undefined ||
environment.running_ip === null ||
environment.running_ip.length === 0
?
"Not running. Click to get the start command that can be ran on your server."
:
<span>IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus.</span>
:
"Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions."
} placement="top">
<Tooltip title={
<Typography variant="body1" style={{margin: 10, }}>
{environment.Type !== "cloud"
? environment.running_ip === undefined ||
environment.running_ip === null ||
environment.running_ip.length === 0
?
"Not running. Click to get the start command that can be ran on your server."
:
<span>IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus.</span>
:
"Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions."
}
<br />
<br />
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"}
</Typography>
} placement="top">
<Typography
style={{
minWidth: 100,
@@ -1411,7 +1420,7 @@ const EnvironmentTab = memo((props) => {
<Collapse in={listItemExpanded === index} timeout="auto" unmountOnExit>
<Grid container justifyContent="center" style={{minWidth: 850, maxWidth: 850, }}>
<Grid item xs={12} sm={8} md={6}>
<div style={{minWidth: 700, maxWidth: 700, minHeight: 350, display: 'flex', justifyContent: "center", backgroundColor: "transparent", }}>
<div style={{minWidth: 750, maxWidth: 750, minHeight: 350, display: 'flex', justifyContent: "center", backgroundColor: "transparent", }}>
<div style={{ paddingTop: 50, paddingBottom: 100, }}>
<Typography variant="h6">
Self-Hosted Orborus instance
+2 -2
View File
@@ -89,7 +89,6 @@ const Files = memo((props) => {
console.log('escape pressed')
setRenderTextBox(false);
}
}
const changeDistribution = (id, selectedSubOrg) => {
@@ -1051,6 +1050,7 @@ const Files = memo((props) => {
</Dialog>
</FormControl>
) : null}
<div style={{display: "inline-flex", position:"relative", top: 8}}>
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
@@ -1263,7 +1263,7 @@ const Files = memo((props) => {
}
const isDistributed = file?.suborg_distribution?.length > 0 ? true : false;
const filenamesplit = file.filename.split(".")
const iseditable = file.filesize < 2000000 && file.status === "active" && allowedFileTypes.includes(filenamesplit[filenamesplit.length-1])
const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes("."))
return (
<ListItem
key={index}
+70 -36
View File
@@ -29,6 +29,7 @@ import {
Fade,
Portal,
Collapse,
Tooltip,
} from "@mui/material";
import theme from "../theme.jsx";
import RecentWorkflow from "../components/RecentWorkflow.jsx";
@@ -56,7 +57,7 @@ const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
const navigate = useNavigate();
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, setSearchBarModalOpen, searchBarModalOpen} = useContext(Context);
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, setSearchBarModalOpen, searchBarModalOpen, isDocSearchModalOpen} = useContext(Context);
const [expandLeftNav, setExpandLeftNav] = useState(false);
const [activeOrgName, setActiveOrgName] = useState(
@@ -86,21 +87,21 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
return orgOptions.find((option) => option.name === selectedOrg);
}, [selectedOrg, orgOptions]);
//With this code it is opening search bar on google chrome search bar as well which is not required
useEffect(() => {
const handleKeyDown = (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
setSearchBarModalOpen((prev)=> !prev);
}
};
//With this code it is opening search bar on google chrome search bar as well which is not required
useEffect(() => {
const handleKeyDown = (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
setSearchBarModalOpen((prev)=> !prev);
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [setSearchBarModalOpen]);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [setSearchBarModalOpen]);
const CustomPopper = (props) => {
return (
@@ -154,6 +155,7 @@ useEffect(() => {
>
{props.children}
</Box>
<Link to="/admin?tab=tenants" style={hrefStyle}>
<Box
sx={{
@@ -524,7 +526,7 @@ useEffect(() => {
<Divider style={{ marginBottom: 10, }} />
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: 2.0.0-rc6
Version: 2.0.1
</Typography>
</Menu>
</span>
@@ -720,11 +722,7 @@ useEffect(() => {
margin_left:
org.creator_org !== undefined &&
org.creator_org !== null &&
org.creator_org.length > 0
? org.id === userdata.active_org.id
? 0
: 20
: 0,
org.creator_org.length > 0 ? 20 : 0,
};
}) || []
);
@@ -775,6 +773,9 @@ useEffect(() => {
}, [window?.location?.pathname]);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
return (
<div
style={{
@@ -787,12 +788,13 @@ useEffect(() => {
transition: "width 0.3s ease",
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.2)" ,
resize: 'both',
zoom: 0.8,
zoom: isSafari ? undefined : 0.8,
transform: isSafari ? "scale(0.8)" : undefined,
transformOrigin: isSafari ? "top left" : undefined,
height: "calc((100vh - 32px)*1.2)",
}}
>
{searchBarModalOpen ? <ModalView serverside={serverside} userdata={userdata} searchBarModalOpen={searchBarModalOpen} setSearchBarModalOpen={setSearchBarModalOpen} globalUrl={globalUrl} /> : null}
{searchBarModalOpen && !isDocSearchModalOpen ? <ModalView serverside={serverside} userdata={userdata} searchBarModalOpen={searchBarModalOpen} setSearchBarModalOpen={setSearchBarModalOpen} globalUrl={globalUrl} isDocSearchModalOpen={isDocSearchModalOpen} /> : null}
<Box
sx={{
display: "flex",
@@ -812,14 +814,36 @@ useEffect(() => {
setExpandLeftNav(false)
}
}}
>
<a href="/" style={{ textDecoration: "none" }}>
<img
src={ShuffleLogo}
alt="Shuffle Logo"
style={{ width: 24, height: 24 }}
/>
</a>
>
<Tooltip
title="Go to Home"
placement="top"
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,
}
}
}}
>
<Link to="/">
<img
src={ShuffleLogo}
alt="Shuffle Logo"
style={{ width: 24, height: 24 }}
/>
</Link>
</Tooltip>
<Box
sx={{
display: "flex",
@@ -1454,7 +1478,7 @@ useEffect(() => {
: "#C8C8C8"
}}
>
Shuffle Agent
Hybrid Locations
</span>
</Button>
</Link>
@@ -1586,6 +1610,18 @@ useEffect(() => {
marginLeft: 5,
}}
>
{expandLeftNav &&
<Button
variant="outlined"
style={{marginBottom: 15, borderWidth: 2, }}
onClick={() => {
window.open("https://shuffler.io/contact?category=book_a_demo", "_blank")
}}
>
Book a Demo
</Button>
}
<Box ref={autocompleteRef}>
<Autocomplete
disablePortal
@@ -1879,13 +1915,11 @@ useEffect(() => {
};
export default LeftSideBar;
const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata}) => {
const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata, isDocSearchModalOpen}) => {
return (
(
<Dialog
open={searchBarModalOpen}
open={searchBarModalOpen && !isDocSearchModalOpen}
onClose={() => {
setSearchBarModalOpen(false);
}}
+69 -20
View File
@@ -46,7 +46,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 } = props;
//const alert = useAlert();
let navigate = useNavigate();
const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false);
@@ -165,11 +165,25 @@ const LicencePopup = (props) => {
]
}
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
@@ -255,7 +269,7 @@ const LicencePopup = (props) => {
style={{ borderRadius: theme.palette?.borderRadius, }}
placement="bottom"
>
<div style={{}}>
<div style={{ backgroundColor: "#1e1e1e"}}>
<Paper
style={newPaperstyle}
// onMouseEnter={() => setHovered(true)}
@@ -343,9 +357,9 @@ const LicencePopup = (props) => {
</div>
</DialogContent>
</Dialog>
<Button style={{ backgroundColor: '#2F2F2F', color: "white", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', width: 144, height: 40 }}>Current Plan</Button>
{subscription.active === true && !isScale && <Button style={{ backgroundColor: '#2F2F2F', color: "white", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', width: 144, height: 40 }}>Current Plan</Button> }
<div style={{ display: "flex" }}>
{top_text === "Base Cloud Access" && userdata.has_card_available === true ?
{top_text === "Base Cloud Access" && userdata.has_card_available === true && !isScale ?
<Chip
style={{
backgroundColor: "#f86a3e",
@@ -511,7 +525,10 @@ const LicencePopup = (props) => {
"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 >= 300000 ?
"You have subscribed to the Enterprise plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." :
`You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.`
:
`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.`
}
@@ -699,8 +716,33 @@ const LicencePopup = (props) => {
setCalculatedCost("$960")
setSelectedValue(8)
} else {
setCalculatedCost("$960")
setSelectedValue(300)
if (userdata && userdata?.app_execution_limit) {
if (userdata.app_execution_limit >= 300000 && userdata.app_execution_limit < 400000) {
setSelectedValue(400)
setCalculatedCost("$1280")
}else if (userdata?.app_execution_limit >= 400000 && userdata?.app_execution_limit < 500000) {
setSelectedValue(500)
setCalculatedCost("$1600")
} else if (userdata?.app_execution_limit >= 500000 && userdata?.app_execution_limit < 600000) {
setSelectedValue(600)
setCalculatedCost("$1920")
} else if (userdata?.app_execution_limit >= 600000 && userdata?.app_execution_limit < 700000) {
setSelectedValue(700)
setCalculatedCost("$2240")
} else if (userdata?.app_execution_limit >= 700000 && userdata?.app_execution_limit < 800000) {
setSelectedValue(800)
setCalculatedCost("$2560")
} else if (userdata?.app_execution_limit >= 800000 && userdata?.app_execution_limit < 900000) {
setSelectedValue(900)
setCalculatedCost("$2880")
}else {
setCalculatedCost("$960")
setSelectedValue(300)
}
}else {
setCalculatedCost("$960")
setSelectedValue(300)
}
}
}, [shuffleVariant])
@@ -864,20 +906,26 @@ const LicencePopup = (props) => {
return
}
const priceItem = window.location.origin === "https://shuffler.io" ?
shuffleVariant === 0 ? "app_executions" : "cores"
:
shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9"
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, shuffleVariant)
console.log("Priceitem: ", priceItem, quantity, shuffleVariant)
var checkoutObject = {
lineItems: [
{
price: priceItem,
quantity: shuffleVariant === 0 ? selectedValue / 100 : selectedValue,
quantity: quantity,
},
],
mode: "subscription",
@@ -973,7 +1021,7 @@ const LicencePopup = (props) => {
</span>
: null}
{isCloud &&
{/* {isCloud &&
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 ?
@@ -994,7 +1042,7 @@ const LicencePopup = (props) => {
/>
)
})
: null}
: null} */}
</Grid>
<Grid item xs={8}>
<Grid style={{}}>
@@ -1002,12 +1050,12 @@ const LicencePopup = (props) => {
<Card style={{
padding: 20,
borderRadius: theme.palette?.borderRadius,
border: "1px solid #f85a3e",
border: !isScale ? "1px solid #f85a3e" : 'none',
}}>
<div>
<Button style={{ backgroundColor: 'rgba(255, 132, 68, 0.2)', color: "#FF8444", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', }}
{ !isScale && <Button style={{ backgroundColor: 'rgba(255, 132, 68, 0.2)', color: "#FF8444", textTransform: "capitalize", borderRadius: 200, boxShadow: 'none', }}
variant="contained"
color="primary">Recommended </Button>
color="primary">Recommended </Button> }
</div>
<Typography variant="h6" style={{ marginTop: 10, marginBottom: 10, flex: 5, }}>{shuffleVariant === 1 ? "Scale" : "Enterprise"}</Typography>
@@ -1095,6 +1143,7 @@ const LicencePopup = (props) => {
>
View all plans
</Button>
<Button
variant="contained"
style={{ borderRadius: 4, textTransform: "capitalize", color: "#1a1a1a", backgroundColor: "#ff8544", width: "100%", fontSize: 16}}
File diff suppressed because it is too large Load Diff
+228 -211
View File
@@ -74,7 +74,7 @@ const OrgHeaderexpandedNew = (props) => {
getContentAnchorEl: () => null,
};
const [orgName, setOrgName] = useState(selectedOrganization?.name);
const [orgName, setOrgName] = useState(selectedOrganization?.name);
const [orgDescription, setOrgDescription] = React.useState(
selectedOrganization.description
);
@@ -180,41 +180,41 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username)
const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token)
const [regionStatus, setRegionStatus] = useState();
useEffect(() => {
if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) {
setDocumentationReference(selectedOrganization?.defaults?.documentation_reference)
}
if (uploadRepo !== selectedOrganization?.defaults?.workflow_upload_repo){
if (uploadRepo !== selectedOrganization?.defaults?.workflow_upload_repo) {
setUploadRepo(selectedOrganization?.defaults?.workflow_upload_repo)
}
if (uploadBranch !== selectedOrganization?.defaults?.workflow_upload_branch){
if (uploadBranch !== selectedOrganization?.defaults?.workflow_upload_branch) {
setUploadBranch(selectedOrganization?.defaults?.workflow_upload_branch)
}
if (uploadUsername !== selectedOrganization?.defaults?.workflow_upload_username){
if (uploadUsername !== selectedOrganization?.defaults?.workflow_upload_username) {
setUploadUsername(selectedOrganization?.defaults?.workflow_upload_username)
}
if (uploadToken !== selectedOrganization?.defaults?.workflow_upload_token){
if (uploadToken !== selectedOrganization?.defaults?.workflow_upload_token) {
setUploadToken(selectedOrganization?.defaults?.workflow_upload_token)
}
}, [selectedOrganization])
useEffect(() => {
if (selectedOrganization !== undefined && selectedOrganization !== null) {
if((orgName === undefined || orgName === null || orgName.length === 0) && selectedOrganization?.name !== orgName) {
if ((orgName === undefined || orgName === null || orgName.length === 0) && selectedOrganization?.name !== orgName) {
setOrgName(selectedOrganization?.name)
}
if((orgDescription === undefined || orgDescription === null || orgDescription.length === 0) && selectedOrganization?.description !== orgDescription) {
if ((orgDescription === undefined || orgDescription === null || orgDescription.length === 0) && selectedOrganization?.description !== orgDescription) {
setOrgDescription(selectedOrganization?.description)
}
}
}, [selectedOrganization])
const handleEditOrg = (
name,
description,
@@ -262,21 +262,21 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
});
}
const handleSendChangeRegionMail = (region)=> {
if(selectedOrganization === undefined || selectedOrganization === null){
const handleSendChangeRegionMail = (region) => {
if (selectedOrganization === undefined || selectedOrganization === null) {
toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
return
}
let destinationRegion = region;
if (region === "US") {
destinationRegion = "us-west2";
} else if (region === "EU") {
destinationRegion = "europe-west3";
} else if (region === "CA") {
destinationRegion = "northamerica-northeast1";
} else if (region === "UK") {
destinationRegion = "europe-west2";
}
const regionToCloudRegion = {
'US': 'us-west2',
'EU': 'europe-west3',
'CA': 'northamerica-northeast1',
'UK': 'europe-west2',
'EU-2': 'europe-west3'
};
const destinationRegion = regionToCloudRegion[region] || region;
var data = {
dst_region: destinationRegion
@@ -284,7 +284,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
toast.info("Sending request for changing region to " + region)
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change/region/request`,{
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change/region/request`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -292,13 +292,13 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
credentials: "include",
body: JSON.stringify(data),
}).then((response)=>{
if(response.status !== 200){
}).then((response) => {
if (response.status !== 200) {
toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
}else{
toast.success("successfully send request for changing region. We will contact you shortly.")
} else {
toast.success("Successfully sent request for region change. We will process the move and contact you shortly.")
}
}).catch((err)=>{
}).catch((err) => {
console.log(err)
toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
})
@@ -307,17 +307,16 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
const setSelectedRegion = (region) => {
// send a POST request to /api/v1/orgs/{org_id}/region with the region as the body
if(region === "US") {
region = "us-west2"
} else if(region === "EU") {
region = "europe-west2"
} else if(region === "CA") {
region = "northamerica-northeast1"
} else if(region === "UK") {
region = "europe-west2"
} else if (region === "EU-2") {
region = "europe-west3"
}
const regionMap = {
"US": "us-west2",
"EU": "europe-west2",
"CA": "northamerica-northeast1",
"UK": "europe-west2",
"EU-2": "europe-west3",
"AUS": "australia-southeast1"
};
region = regionMap[region] || region;
var data = {
dst_region: region
@@ -336,7 +335,11 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
timeOut: 1000
}).then((response) => {
if (response.status !== 200) {
toast("Failed to change region!")
response.json().then((reason) => {
toast.error("Failed to change region: " + reason.reason)
}).catch((err) => {
toast.error("Failed to change region")
});
}
else {
toast("Region changed successfully! Reloading in 5 seconds..")
@@ -347,7 +350,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
}
return response.json();
// return responseJson
})
}
@@ -355,7 +358,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
const orgSaveButton = (
<Tooltip title="Save any unsaved data" placement="bottom">
<Button
style={{ width: 244, height: 51, display: 'flex', justifyContent:'center', textTransform: 'capitalize', padding: "16px, 24px, 16px, 24px", borderRadius: 4, backgroundColor: "#ff8544", color: "#1a1a1a", fontSize: 16, }}
style={{ width: 244, height: 51, display: 'flex', justifyContent: 'center', textTransform: 'capitalize', padding: "16px, 24px, 16px, 24px", borderRadius: 4, backgroundColor: "#ff8544", color: "#1a1a1a", fontSize: 16, }}
variant="contained"
color="primary"
disabled={
@@ -444,29 +447,29 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
<div style={{ flex: "3", color: "white" }}>
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<div style={{width: "100%", maxWidth: 434, marginRight: 10}}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
Name
<TextField
required
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 434,
marginTop: "5px",
marginRight: "15px",
backgroundColor: isEditOrgTab ? "#212121" : theme.palette.inputColor,
}}
fullWidth={true}
placeholder="Name"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={orgName}
onBlur={() => {
if((orgName !== selectedOrganization?.name) && (orgName !== "")) {
<TextField
required
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 434,
marginTop: "5px",
marginRight: "15px",
backgroundColor: isEditOrgTab ? "#212121" : theme.palette.inputColor,
}}
fullWidth={true}
placeholder="Name"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={orgName}
onBlur={() => {
if ((orgName !== selectedOrganization?.name) && (orgName !== "")) {
handleEditOrg(
orgName,
orgDescription,
@@ -497,61 +500,62 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
auto_provision: selectedOrganization?.sso_config?.auto_provision,
}
)
}}}
onChange={(e) => {
if (e.target.value.length > 100) {
toast("Choose a shorter name.");
return;
}
}
}}
onChange={(e) => {
if (e.target.value.length > 100) {
toast("Choose a shorter name.");
return;
}
setOrgName(e.target.value);
}}
color="primary"
InputProps={{
style: {
color: "white",
height: "35px",
fontSize: "1em",
borderRadius: 4,
},
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
}}
/>
setOrgName(e.target.value);
}}
color="primary"
InputProps={{
style: {
color: "white",
height: "35px",
fontSize: "1em",
borderRadius: 4,
},
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
}}
/>
</div>
{userdata?.support ? (
<div style={{ alignItems: 'center' }}>
<div style={{ marginRight: '12px', color: 'white' }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4 }}
id="multiselect-status"
multiple
value={selectedStatus}
onChange={(event) => {handleStatusChange(event);setSelectedStatus(event.target.value)}}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "old customer", "old lead"].map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
): null}
<div style={{ marginRight: '12px', color: 'white' }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4 }}
id="multiselect-status"
multiple
value={selectedStatus}
onChange={(event) => { handleStatusChange(event); setSelectedStatus(event.target.value) }}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "old customer", "old lead"].map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
) : null}
{isCloud ? (
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }} >
Region
<RegionChangeModal selectedOrganization={selectedOrganization} setSelectedRegion={setSelectedRegion} userdata={userdata} handleSendChangeRegionMail={handleSendChangeRegionMail}/>
</div>
): null}
Change Region
<RegionChangeModal selectedOrganization={selectedOrganization} setSelectedRegion={setSelectedRegion} userdata={userdata} handleSendChangeRegionMail={handleSendChangeRegionMail} />
</div>
) : null}
</div>
<div style={{ marginTop: "10px" }} />
About
@@ -575,38 +579,40 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
variant="outlined"
placeholder="A description for the organization"
value={orgDescription}
onBlur={() => {if((orgDescription !== selectedOrganization?.description) && (orgDescription !== "")) {
handleEditOrg(
orgName,
orgDescription,
selectedOrganization.id,
selectedOrganization.image,
{
app_download_repo: selectedOrganization?.defaults?.app_download_repo,
app_download_branch: selectedOrganization?.defaults?.app_download_branch,
workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
notification_workflow: selectedOrganization?.defaults?.notification_workflow,
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: !newsletter,
weekly_recommendations: !weeklyRecommendations,
},
{
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,
}
)
}}}
onBlur={() => {
if ((orgDescription !== selectedOrganization?.description) && (orgDescription !== "")) {
handleEditOrg(
orgName,
orgDescription,
selectedOrganization.id,
selectedOrganization.image,
{
app_download_repo: selectedOrganization?.defaults?.app_download_repo,
app_download_branch: selectedOrganization?.defaults?.app_download_branch,
workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
notification_workflow: selectedOrganization?.defaults?.notification_workflow,
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: !newsletter,
weekly_recommendations: !weeklyRecommendations,
},
{
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,
}
)
}
}}
onChange={(e) => {
setOrgDescription(e.target.value);
}}
@@ -626,7 +632,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
</div>
</div>
<Typography variant="h5" style={{ color: "rgba(241, 241, 241, 1)", fontSize: 24, fontWeight: 600, marginTop: 40, textAlign: "left" }}>
<Typography variant="h5" style={{ color: "rgba(241, 241, 241, 1)", fontSize: 24, fontWeight: 600, marginTop: 40, textAlign: "left" }}>
Preferences
</Typography>
@@ -647,9 +653,9 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
</Grid>
<Grid item xs={12}>
<span>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Org Documentation reference</Typography>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Org Documentation reference</Typography>
<Typography variant="body2" color="textSecondary" style={{ fontWeight: 400, fontSize: 16, marginTop: 8 }}>
<Typography variant="body2" color="textSecondary" style={{ fontWeight: 400, fontSize: 16, marginTop: 8 }}>
Add a URL that is added as a link, pointing to any external documentation page you want.
</Typography>
@@ -672,7 +678,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
placeholder="Paste a URL to an external reference for this implementation"
value={documentationReference}
onBlur={() => {
if(documentationReference !== selectedOrganization?.defaults?.documentation_reference) {
if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) {
handleEditOrg(
orgName,
orgDescription,
@@ -714,7 +720,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -728,16 +734,16 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
globalUrl={globalUrl}
userdata={userdata}
serverside={false}
/>
/>
<Grid item xs={12} style={{ marginTop: 20, }}>
<Typography variant="h4" style={{ textAlign: "left", color: "rgba(241, 241, 241, 1)", fontSize: 24, fontWeight: 600, }}>Workflow Backup Repository</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: "#9E9E9E", fontSize: 16, fontWeight: 400 }}>
Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the repo root in the /orgId/workflow-status/workflowId.json format. <b>MSSP:</b> If suborg exists, this will automatically be applied for them as well (not retroactive). <a href="/docs/configuration#environment-variables" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">Credentials are encrypted.</a>
<Typography variant="h4" style={{ textAlign: "left", color: "rgba(241, 241, 241, 1)", fontSize: 24, fontWeight: 600, }}>Workflow Backup Repository</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: "#9E9E9E", fontSize: 16, fontWeight: 400 }}>
Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the repo root in the /orgId/workflow-status/workflowId.json format. <b>MSSP:</b> If suborg exists, this will automatically be applied for them as well (not retroactive). <a href="/docs/configuration#environment-variables" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">Credentials are encrypted.</a>
</Typography>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Repository for workflow backup</Typography>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Repository for workflow backup</Typography>
<TextField
required
style={{
@@ -765,7 +771,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -777,7 +783,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Branch</Typography>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Branch</Typography>
<TextField
required
style={{
@@ -805,7 +811,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -819,7 +825,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Username for backup of workflows</Typography>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Username for backup of workflows</Typography>
<TextField
required
style={{
@@ -847,7 +853,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -859,7 +865,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Git token/password</Typography>
<Typography style={{ fontWeight: 400, fontSize: 16 }}>Git token/password</Typography>
<TextField
required
style={{
@@ -886,7 +892,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -930,7 +936,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -970,7 +976,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -1010,7 +1016,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -1050,7 +1056,7 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
},
style: {
color: "white",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
@@ -1080,14 +1086,15 @@ const [orgName, setOrgName] = useState(selectedOrganization?.name);
export default OrgHeaderexpandedNew;
const RegionChangeModal = memo(({selectedOrganization, setSelectedRegion, userdata, handleSendChangeRegionMail}) => {
const RegionChangeModal = memo(({ selectedOrganization, setSelectedRegion, userdata, handleSendChangeRegionMail }) => {
// Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"
// var regions = ["us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"]
const regionMapping = {
"US": "us",
"EU-2": "eu",
"CA": "ca",
"UK": "gb"
"UK": "gb",
"AUS": "au",
};
//let regiontag = "UK";
@@ -1095,6 +1102,7 @@ const RegionChangeModal = memo(({selectedOrganization, setSelectedRegion, userda
let regionCode = "gb";
const regionsplit = selectedOrganization?.region_url?.split(".");
if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) {
const namesplit = regionsplit[0]?.split("/");
regiontag = namesplit[namesplit.length - 1];
@@ -1103,58 +1111,67 @@ const RegionChangeModal = memo(({selectedOrganization, setSelectedRegion, userda
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU";
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
} else if (regiontag === "austrailia") {
regiontag = "AUS";
regionCode = "au"
}
}
return (
<FormControl style={{ display: "flex", flexDirection: "column", marginTop: 5, alignItems: "center" }} >
{/* <InputLabel id="demo-simple-select-label">Region</InputLabel> */}
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={regiontag}
style={{minWidth: 120, height: 35, borderRadius: 4 }}
onChange={(e) => {
if(userdata?.support){
setSelectedRegion(e.target.value)
}else{
handleSendChangeRegionMail(e.target.value)
}
}}
>
{Object.keys(regionMapping).map((region, index) => {
const regionImageCode = regionMapping[region];
// Set the default region if selectedOrganization.region is not set
if (selectedOrganization.region === undefined) {
selectedOrganization.region = "europe-west2";
}
// Check if the current region matches the selected region
if (region === selectedOrganization.region) {
// If the region matches, set the MenuItem as selected
return (
<MenuItem value={region} key={index} disabled>
{/* show region image through cdn */}
<img src={`https://flagcdn.com/48x36/${regionImageCode}.png`} alt={region} style={{ marginRight: 10 }} />
return (
<FormControl style={{ display: "flex", flexDirection: "column", marginTop: 5, alignItems: "center" }} >
{/* <InputLabel id="demo-simple-select-label">Region</InputLabel> */}
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={regiontag}
style={{ minWidth: 120, height: 35, borderRadius: 4 }}
onChange={(e) => {
// if (userdata?.support) {
// setSelectedRegion(e.target.value)
// } else {
// handleSendChangeRegionMail(e.target.value)
// }
setSelectedRegion(e.target.value)
}}
>
{Object.keys(regionMapping).map((region, index) => {
const regionImageCode = regionMapping[region];
// Set the default region if selectedOrganization.region is not set
if (selectedOrganization.region === undefined) {
selectedOrganization.region = "europe-west2";
}
if (region === "AUS") {
region = "AUS (test)"
}
// Check if the current region matches the selected region
if (region === selectedOrganization.region) {
// If the region matches, set the MenuItem as selected
return (
<MenuItem value={region} key={index} disabled>
{/* show region image through cdn */}
<img src={`https://flagcdn.com/48x36/${regionImageCode}.png`} alt={region} style={{ marginRight: 10 }} />
{region}
</MenuItem>
);
} else {
return <MenuItem sx={{ display: 'flex' }} key={index} value={region}>
<img
src={`https://flagcdn.com/48x36/${regionImageCode}.png`}
alt={region}
style={{ marginRight: 10, width: 20, height: 18, }}
/>
{region}
</MenuItem>
);
} else {
return <MenuItem sx={{display: 'flex'}} key={index} value={region}>
<img
src={`https://flagcdn.com/48x36/${regionImageCode}.png`}
alt={region}
style={{ marginRight: 10, width: 20, height: 18,}}
/>
{region}
</MenuItem>;
}
})}
</Select>
</FormControl>
);
}
})}
</Select>
</FormControl>
);
})
@@ -3,7 +3,6 @@ import { Link, useNavigate, useLocation } from "react-router-dom";
import Billing from "../components/Billing.jsx";
import Priorities from "../components/Priorities.jsx";
import Branding from "../components/Branding.jsx";
import AnalyticsTab from '../components/AnalyticsTab.jsx';
import EditOrgTab from '../components/EditOrgTab.jsx';
import CloudSyncTab from '../components/CloudSyncTab.jsx';
import SSOTab from "../components/ssoTab.jsx"
+115 -44
View File
@@ -214,6 +214,20 @@ const ParsedAction = (props) => {
}
}, [expansionModalOpen])
/*
useEffect(() => {
// This will have the OLD selectedAction, not the new one huh?
// How do we map the fields correctly?
if (selectedAction === undefined || selectedAction === null) {
console.log("Selected action is undefined")
return
}
console.log("Selected action: ", selectedAction?.name, selectedAction)
}, [selectedAction])
*/
useEffect(() => {
// Changes the order of params to show in order:
// auth, required, optional
@@ -347,7 +361,7 @@ const ParsedAction = (props) => {
}
if (keyorder.join(",") !== newkeyorder.join(",")) {
//toast("KEYORDER CHANGED!")
//console.log("KEYORDER CHANGED! DID ACTION AS WELL?", keyorder, newkeyorder)
setSelectedActionParameters(newparams)
selectedAction.parameters = newparams
@@ -688,7 +702,7 @@ const ParsedAction = (props) => {
if (newActionList.find((item) => item.type === "Shuffle DB") === undefined) {
let cacheKey = {
type: "Shuffle DB",
name: "Shuffle DB",
name: "Shuffle Datastore",
value: "$shuffle_cache",
highlight: "shuffle_cache",
autocomplete: "shuffle_cache",
@@ -753,44 +767,78 @@ const ParsedAction = (props) => {
if (parents.length > 1) {
const labels = [];
for (let parentNode of parents) {
if (parentNode.label !== "Runtime Argument" && !labels.includes(parentNode.label)) {
labels.push(parentNode.label);
let exampleData = parentNode.example ?? "";
if (!exampleData && workflowExecutions.length > 0) {
for (let exec of workflowExecutions) {
const foundResult = exec.results?.find(result => result.action.id === parentNode.id);
if (foundResult) {
const valid = validateJson(foundResult.result);
if (valid.valid && valid.result.success !== false) {
exampleData = valid.result;
break;
}
if (parentNode.label === "Runtime Argument" || labels.includes(parentNode.label)) {
continue
}
labels.push(parentNode.label);
let exampleData = parentNode.example ?? "";
if (parentNode?.app_name === "http") {
exampleData = ""
}
if (workflowExecutions.length > 0) {
for (let exec of workflowExecutions) {
const foundResult = exec.results?.find(result => result?.action?.id === parentNode?.id);
if (foundResult) {
const valid = validateJson(foundResult.result);
if (valid.valid && valid.result.success !== false) {
exampleData = valid.result
break
}
}
}
if (parentNode.label === undefined) {
parentNode.label = ""
}
newActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
parentActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
}
if (exampleData === "" && apps !== undefined && apps !== null && apps?.length > 0) {
// Check apps if it exists, then if it
const foundApp = apps?.find(app => app?.id === parentNode?.app_id)
if (foundApp !== undefined && foundApp !== null) {
if (foundApp?.generated === true || foundApp?.name === "http") {
const validationData = validateJson(`{
"status": 200,
"body": {
"example": "json",
"values": "json"
},
"url": "https://example.com",
"headers": {
"Content-Type": "application/json",
"Example-Header": "two"
},
"cookies": {
"example": "session",
"__session": "sessionid"
},
"success": true
}`)
if (validationData.valid) {
exampleData = validationData.result
}
}
}
}
if (parentNode.label === undefined) {
parentNode.label = ""
}
newActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
parentActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
}
}
}
@@ -838,10 +886,10 @@ const ParsedAction = (props) => {
}
}
return { ...param, value: paramvalue, error: message }
});
})
setSelectedActionParameters(newParameters);
setActionlist(newActionList);
setSelectedActionParameters(newParameters)
setActionlist(newActionList)
}, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents, setNewSelectedAction]);
useEffect(() => {
@@ -1112,6 +1160,12 @@ const ParsedAction = (props) => {
selectedActionParameters[1].value = splitparsed[1]
selectedAction.parameters[1].value = splitparsed[1]
if (splitparsed.length > 2) {
toast.warn("Filter list only supports filtering at the first level. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.")
} else if (selectedAction.parameters[1].value.includes(".#")) {
toast.warn("This filter may not work due to using .# indexing. Please use the 'execute python' action and try the 'filter a list' function in the code editor.")
}
} else {
// Remove .# and after
const splitparsed = parsedvalue.split(".#")
@@ -2497,7 +2551,7 @@ const ParsedAction = (props) => {
{workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ? (
<div style={{ marginTop: "20px" }}>
<Typography>Execution variable (optional)</Typography>
<Typography color="textSecondary">Runtime variable (optional)</Typography>
<Select
MenuProps={{
disableScrollLock: true,
@@ -2575,7 +2629,8 @@ const ParsedAction = (props) => {
</div>
*/}
{setNewSelectedAction !== undefined ? (
{isAgent ? null :
setNewSelectedAction !== undefined ? (
<Autocomplete
id="action_search"
disabled={isAgent || (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0)}
@@ -3609,6 +3664,20 @@ const ParsedAction = (props) => {
</Box>
);
if (selectedApp.name === "email") {
//hideBody = false
showButtonField = false
hideBodyButtonValue = null
}
if ((multiline === undefined || multiline === false) && ((data?.autocompleted === true || data?.field_active === true) || data.name.startsWith("${") && data.name.endsWith("}"))) {
multiline = true
}
if (data?.autocompleted === true || data?.field_active === true) {
rows = "1"
}
var datafield = (
<Tooltip
@@ -3690,7 +3759,8 @@ const ParsedAction = (props) => {
setScrollConfig(scrollConfig)
}
}}
rows={data.name.startsWith("${") && data.name.endsWith("}") ? 2 : rows}
minRows={rows}
maxRows={6}
color="primary"
// defaultValue={data.value}
value={
@@ -3999,7 +4069,8 @@ const ParsedAction = (props) => {
helperText={returnHelperText(data.name, data.value)}
fullWidth
multiline={multiline}
rows={"3"}
minRows={3}
maxRows={6}
color="primary"
defaultValue={data.value}
type={"text"}
@@ -4545,7 +4616,7 @@ const ParsedAction = (props) => {
data.field_active === true ?
<Tooltip
color="primary"
title={"This is an Simplified field to make the body easier to use. NOT required according to the API documentation. If this is filled, it will be overridden in the Advanced Body"}
title={"This is a Simplified Field to make the body easier to use. NOT required according to the API documentation. If this is filled, it will be overridden in the Advanced Body"}
placement="top"
>
<PriorityHighIcon
+5 -20
View File
@@ -354,13 +354,13 @@ const RuntimeDebugger = (props) => {
var imageSource = "";
if (params?.row?.org?.id?.length > 0) {
if (params?.row?.org?.image?.length > 0){
imageSource = params.row.org.image
imageSource = params?.row.org?.image
}else {
imageSource = "/images/no_image.png"
}
}else {
if (userdata.active_org.image?.length > 0){
imageSource = userdata.active_org.image
if (userdata?.active_org.image?.length > 0){
imageSource = userdata?.active_org?.image
}else {
imageSource = "/images/no_image.png"
}
@@ -370,7 +370,7 @@ const RuntimeDebugger = (props) => {
<span style={{}} onClick={() => {
//setStatus(params.row.status)
}}>
{userdata?.active_org?.creator_org?.length === 0 ? (
{userdata?.active_org?.creator_org?.length === 0 && suborgWorkflowRuns ? (
<img src={imageSource} alt={source} style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
) : null}
<Tooltip title={source} placement="top">
@@ -924,28 +924,13 @@ const RuntimeDebugger = (props) => {
onClick={() => setSearchQuery('')}
/>
)}
<button
type="button"
style={{
color: "#1A1A1A",
border: "none",
padding: "10px 20px",
width: 100,
height: 35,
borderRadius: 4,
backgroundColor: "#FF8544",
cursor: "pointer",
}}
>
Search
</button>
</InputAdornment>
),
}}
onChange={(e)=>{handleQueryChange(e)}}
color="primary"
placeholder="Filter by Workflow Name, Status, Execution Argument, Results.."
placeholder="Filter by Workflow Name, Status, Execution Argument, Results"
id="shuffle_search_field"
/>
</div>
+82 -42
View File
@@ -51,12 +51,13 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
const SearchData = props => {
const { serverside, globalUrl, userdata } = props
let navigate = useNavigate();
const { searchBarModalOpen, setSearchBarModalOpen } = useContext(Context);
const { searchBarModalOpen, setSearchBarModalOpen, isDocSearchModalOpen } = useContext(Context);
const borderRadius = 3
const node = useRef()
const [searchOpen, setSearchOpen] = useState(false)
const [oldPath, setOldPath] = useState("")
const [value, setValue] = useState("");
const isDocSearchModal = isDocSearchModalOpen;
const handleLinkClick = () => {
if (searchBarModalOpen) {
@@ -90,6 +91,8 @@ const SearchData = props => {
const [inputValue, setInputValue] = useState(currentRefinement);
const textFieldRef = useRef(null);
const debounceTimeoutRef = useRef(null);
const keyPressHandler = (e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -109,6 +112,27 @@ const SearchData = props => {
}
}, [searchOpen]);
useEffect(() => {
// Clear any existing timeout to prevent multiple executions
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
// Set a new timeout that will execute the search after a 200ms delay
debounceTimeoutRef.current = setTimeout(() => {
refine(inputValue);
setSearchOpen(inputValue.trim() !== '');
}, 200);
// Cleanup function that runs when component unmounts or when dependencies change
// This ensures we don't have any hanging timeouts
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
};
}, [inputValue, refine]); // Effect runs when inputValue or refine function changes
return (
<form id="search_form" noValidate type="searchbox" action="" role="search" onClick={() => {
@@ -116,7 +140,7 @@ const SearchData = props => {
>
<TextField
fullWidth
style={{ zIndex: 1100, marginTop: -20, marginBottom: 200, position: "fixed", backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: 685, }}
style={{ zIndex: 1100, marginTop: -20, marginBottom: 200, position: "fixed", backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: 690, }}
InputProps={{
style: {
color: "white",
@@ -143,7 +167,7 @@ const SearchData = props => {
autoComplete='off'
type="search"
color="primary"
placeholder="Find Public Apps, Workflows, Documentation..."
placeholder={isDocSearchModal ? "Type to search documentation..." : "Find Public Apps, Workflows, Documentation..."}
value={inputValue}
id="shuffle_search_field"
onClick={(event) => {
@@ -157,12 +181,6 @@ const SearchData = props => {
onChange={(event) => {
const newValue = event.target.value;
setInputValue(newValue);
refine(newValue);
if (newValue.trim() !== '') {
setSearchOpen(true);
} else {
setSearchOpen(false);
}
}}
onKeyDown={keyPressHandler}
inputRef={textFieldRef}
@@ -599,7 +617,11 @@ const SearchData = props => {
}
if (hits.length > 4) {
hits = hits.slice(0, 4)
if(!isDocSearchModal) {
hits = hits.slice(0, 4)
} else {
hits = hits.slice(0, 15)
}
}
const type = "documentation"
@@ -608,15 +630,19 @@ const SearchData = props => {
//console.log(type, hits.length, hits)
return (
<Card elevation={0} style={{ marginRight: 10, marginTop: 50, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 470, boxShadows: "none", }}>
<Card elevation={0} style={{ marginRight: 10, marginTop: isDocSearchModal ? 0 : 50, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 470, boxShadows: "none", }}>
{/* <IconButton style={{ zIndex: 5000, position: "absolute", right: 14, color: "grey" }} onClick={() => {
setSearchOpen(false)
}}>
<CloseIcon />
</IconButton> */}
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", color: "#FF8444", borderBottom: "1px solid", width: 152 }}>
Documentation
</Typography>
{
!isDocSearchModal && (
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", color: "#FF8444", borderBottom: "1px solid", width: 152 }}>
Documentation
</Typography>
)
}
{/*
<IconButton edge="end" aria-label="delete" style={{position: "absolute", top: 5, right: 15,}} onClick={() => {
setSearchOpen(false)
@@ -624,7 +650,7 @@ const SearchData = props => {
<DeleteIcon />
</IconButton>
*/}
<List style={{ backgroundColor: theme.palette.inputColor, }}>
<List style={{ backgroundColor: theme.palette.inputColor, marginTop: isDocSearchModal ? 35 : 0, }}>
{hits.length === 0 ?
<ListItem style={outerlistitemStyle}>
<ListItemAvatar onClick={() => console.log(hits)}>
@@ -648,8 +674,8 @@ const SearchData = props => {
cursor: "pointer",
marginLeft: 5,
marginRight: 5,
maxHeight: 75,
minHeight: 75,
maxHeight: isDocSearchModal ? 100 : 75,
minHeight: isDocSearchModal ? 100 : 75,
maxWidth: 420,
minWidth: "100%",
}
@@ -660,9 +686,13 @@ const SearchData = props => {
(hit.name.charAt(0).toUpperCase() + hit.name.slice(1)).replaceAll("_", " ")
if (name.length > 30) {
name = name.slice(0, 30) + "..."
if(isDocSearchModal) {
name = name.slice(0, 70) + "..."
} else {
name = name.slice(0, 30) + "..."
}
}
const secondaryText = hit.data !== undefined ? hit.data.slice(0, 40) + "..." : ""
const secondaryText = hit.data !== undefined ? hit.data.slice(0, 80) + "..." : ""
const avatar = hit.image_url === undefined ?
baseImage
:
@@ -713,7 +743,7 @@ const SearchData = props => {
</ListItemAvatar>
<ListItemText
primary={name}
secondary={secondaryText}
secondary={<div style={{ padding: '8px 0' }}>{secondaryText}</div>}
/>
{/*
<ListItemSecondaryAction>
@@ -891,28 +921,38 @@ const SearchData = props => {
const modalView = (
<div>
<Grid container style={{ display: "contents", }}>
<Grid item xs="auto" style={{}}>
<Index indexName="appsearch">
<CustomAppHits />
</Index>
{
!isDocSearchModal ? (
<Grid container style={{ display: "contents", }}>
<Grid item xs="auto" style={{}}>
<Index indexName="appsearch">
<CustomAppHits />
</Index>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="workflows">
<CustomWorkflowHits />
</Index>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="documentation">
<CustomDocHits />
</Index>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="discord_chat">
<CustomDiscordHits />
</Index>
</Grid>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="workflows">
<CustomWorkflowHits />
</Index>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="documentation">
<CustomDocHits />
</Index>
</Grid>
<Grid item xs="auto" style={{}}>
<Index indexName="discord_chat">
<CustomDiscordHits />
</Index>
</Grid>
</Grid>
) : (
<Grid item xs="auto" style={{width: "100%"}}>
<Index indexName="documentation">
<CustomDocHits />
</Index>
</Grid>
)
}
</div>
)
@@ -926,7 +966,7 @@ const SearchData = props => {
<CustomSearchBox />
{modalView}
</InstantSearch>
{gettingStartData}
{!isDocSearchModal && gettingStartData}
</div>
)
}
+35 -18
View File
@@ -27,6 +27,7 @@ import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx";
import Draggable from "react-draggable";
import {
Storage as StorageIcon,
FullscreenExit as FullscreenExitIcon,
Extension as ExtensionIcon,
Apps as AppsIcon,
@@ -88,9 +89,10 @@ 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 \"name\": \"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 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, },
@@ -276,6 +278,8 @@ const CodeEditor = (props) => {
parsedPaths = GetParsedPaths(actionlist[i].value, "");
}
} else {
//console.log("EXAMPLE: ", actionlist[i])
// Handle regular action results
if (typeof actionlist[i].example === "object") {
parsedPaths = GetParsedPaths(actionlist[i].example, "");
@@ -654,6 +658,7 @@ const CodeEditor = (props) => {
}
if (!inputvariable.includes(".")) {
inputvariable = inputvariable.toLowerCase()
return inputvariable
}
@@ -662,6 +667,9 @@ const CodeEditor = (props) => {
var removedIndexes = 0
for (var key in itemsplit) {
var tmpitem = itemsplit[key]
if (key == 0) {
tmpitem = tmpitem.toLowerCase()
}
// Makes sure #0 and # are same, as we only visualize first one anyway
if (tmpitem.startsWith("#")) {
@@ -692,20 +700,24 @@ const CodeEditor = (props) => {
var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g)
if (!variable_occurence) {
continue;
continue
}
var new_occurences = variable_occurence.filter((occurrence) => occurrence[0]);
variable_occurence = new_occurences
//var new_occurences = variable_occurence.filter((occurrence) => occurrence[0]);
//variable_occurence = new_occurences
variable_occurence = variable_occurence.filter((occurrence) => occurrence[0]);
// Checks code lines, not variable occurences. Then remaps later
var dollar_occurence = [];
for (let ch = 0; ch < current_code_line.length; ch++) {
//if (current_code_line[ch] === '$' && (ch === 0)) {
if (current_code_line[ch] === '$') {
dollar_occurence.push(ch);
dollar_occurence.push(ch)
}
}
// Lowercase anything between the $ and first .
var dollar_occurence_len = []
try {
for (let occ = 0; occ < variable_occurence.length; occ++) {
@@ -1002,7 +1014,6 @@ const CodeEditor = (props) => {
// Replace quotes with nothing
} else {
console.log("NO TYPE? ", typeof new_input)
try {
new_input = new_input.toString()
} catch (e) {
@@ -1228,7 +1239,7 @@ const CodeEditor = (props) => {
}
const editorLoad = (editor) => {
console.log("EDITOR: ", editor)
//console.log("EDITOR: ", editor)
editor.completers = [customCompleter]
}
@@ -1526,7 +1537,7 @@ const CodeEditor = (props) => {
Source Data
</Typography>
<Typography variant="body2" color="textSecondary">
Drag the data you want into the text editor
Drag the data you want into the text editor! <b>PS: Only support users can see this test-section!</b>
</Typography>
{actionlist?.map((innerdata) => {
@@ -1803,9 +1814,11 @@ const CodeEditor = (props) => {
) : innerdata.type === "workflow_variable" ||
innerdata.type === "execution_variable" ? (
<FavoriteBorderIcon style={{ marginRight: 10 }} />
) : (
) :
innerdata.type === "Shuffle DB" ?
<StorageIcon style={{ marginRight: 10, }} />
:
<ScheduleIcon style={{ marginRight: 10 }} />
);
const handleExecArgumentHover = (inside) => {
var exec_text_field = document.getElementById(
@@ -1875,8 +1888,6 @@ const CodeEditor = (props) => {
menuPosition1.left = 0
}
//console.log("POS1: ", menuPosition1)
return parsedPaths.length > 0 ? (
<NestedMenuItem
key={innerdata.name}
@@ -1894,7 +1905,7 @@ const CodeEditor = (props) => {
overflow: "hidden",
}}
onClick={() => {
console.log(innerdata.example)
//console.log(innerdata.example)
//const handleClick = (item) => {
handleItemClick([innerdata]);
@@ -2246,8 +2257,8 @@ const CodeEditor = (props) => {
{selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ?
"Code to run" :
triggerId ?
`Output : ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)}(${triggerField})` :
`Output : ${appName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + appName?.replaceAll("_", " ").slice(1)}(${fieldName})`
`Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` :
`Output: ${appName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + appName?.replaceAll("_", " ").slice(1)} (${fieldName})`
}
</span>
}
@@ -2429,7 +2440,10 @@ const CodeEditor = (props) => {
variant="outlined"
color="secondary"
onClick={() => {
navigate("")
if (isFileEditor !== true) {
navigate("")
}
setExpansionModalOpen(false);
}}
>
@@ -2453,14 +2467,17 @@ const CodeEditor = (props) => {
}
*/
navigate("")
if (isFileEditor !== true) {
navigate("")
}
// Take localcodedata through the Shuffle JSON parser just in case
// This is to make it so we don't need to handle these fixes on the
// backend by itself
var fixedcodedata = localcodedata
const valid = validateJson(localcodedata, true)
if (valid.valid) {
fixedcodedata = JSON.stringify(valid.result, null, 2)
//fixedcodedata = JSON.stringify(valid.result, null, 2)
fixedcodedata = JSON.stringify(valid.result)
}
// console.log(codedata)
+5 -3
View File
@@ -497,7 +497,9 @@ const TenantsTab = memo((props) => {
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
if (responseJson.reason !== undefined) {
toast(responseJson.reason);
toast.error(responseJson.reason, {
autoClose: 5000,
})
} else {
toast("Failed creating suborg. Please try again");
}
@@ -781,9 +783,9 @@ const TenantsTab = memo((props) => {
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto",overflowX: 'hidden', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' }}>
<div style={{ marginBottom: 20 }}>
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Organizations</h2>
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Tenants</h2>
<span style={{ color: textColor }}>
Control sub organizations (tenants)! {" "}
Create, manage and change to sub-organizations (tenants)! {" "}
{isCloud
? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out."
: ''}
+11 -1
View File
@@ -1335,7 +1335,7 @@ const UserManagmentTab = memo((props) => {
}}
>
<ListItem style={{ width: "100%", padding: "10px 10px 10px 0px", verticalAlign: 'middle', borderBottom: "1px solid #494949", display: "table-row" }}>
{["Username", "API Key", "Role", "Active", "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => (
{["Username", /*"API Key",*/ "Role", /*"Active",*/ "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => (
<ListItemText
key={index}
primary={header}
@@ -1458,6 +1458,8 @@ const UserManagmentTab = memo((props) => {
}}
style={{display:'table-cell', verticalAlign: 'middle' }}
/>
{/*
<ListItemText
style={{
textAlign: "center",
@@ -1488,6 +1490,8 @@ const UserManagmentTab = memo((props) => {
)
}
/>
*/}
<ListItemText
primary={
<Select
@@ -1552,10 +1556,14 @@ const UserManagmentTab = memo((props) => {
}
style={{ display:'table-cell', verticalAlign: 'middle' }}
/>
{/*
<ListItemText
primary={data.active ? "True" : "False"}
style={{display:'table-cell',verticalAlign: 'middle' , padding: "8px", textAlign: "center", color: data.active ? "#02CB70" : "#F53434" }}
/>
*/}
<ListItemText
primary={
data.login_type === undefined ||
@@ -1566,6 +1574,7 @@ const UserManagmentTab = memo((props) => {
}
style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }}
/>
<ListItemText
primary={
data?.mfa_info !== undefined &&
@@ -1576,6 +1585,7 @@ const UserManagmentTab = memo((props) => {
}
style={{ display:'table-cell', verticalAlign: 'middle',padding: "8px", color: data.mfa_info.active ? "#02CB70" : "#F53434" }}
/>
{selectedOrganization?.child_orgs !== undefined &&
selectedOrganization?.child_orgs !== null &&
selectedOrganization?.child_orgs?.length > 0 ? (
@@ -678,7 +678,7 @@ const WorkflowTemplatePopup = (props) => {
}
{img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ?
<Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<div style={{display: "flex", }}>
<div style={{display : dstapp === "NA" ? "none" : "flex" }}>
<TrendingFlatIcon style={{ marginTop: 7, }} />
<div style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} />
@@ -50,12 +50,13 @@ const WorkflowTemplatePopup = (props) => {
setIsClicked,
inputWorkflowId,
inputWorkflow,
onClose,
} = props;
const [isActive, setIsActive] = useState(workflowBuilt === true || (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) || (inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "") ? true : false)
const [isHovered, setIsHovered] = useState(false);
const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true ? true : false)
const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true);
const [errorMessage, setErrorMessage] = useState("");
const [workflowLoading, setWorkflowLoading] = useState(false)
const [showLoginButton, setShowLoginButton] = useState(false);
@@ -173,6 +174,12 @@ const WorkflowTemplatePopup = (props) => {
}
}, [configurationFinished, workflow])
useEffect(() => {
if (isModalOpenDefault === true) {
setModalOpen(true);
}
}, [isModalOpenDefault]);
const imageSize = 32
const defaultBorder = "1px solid rgba(255,255,255,0.6)"
const imagestyleWrapper = {
@@ -516,6 +523,18 @@ const WorkflowTemplatePopup = (props) => {
return false
}
const handleClose = () => {
setModalOpen(false);
if (onClose) {
onClose();
}
if (setIsClicked !== undefined) {
setIsClicked(false);
}
}
const ModalView = () => {
if (modalOpen === false) {
return null
@@ -528,13 +547,7 @@ const WorkflowTemplatePopup = (props) => {
<Drawer
anchor={"right"}
open={modalOpen}
onClose={() => {
setModalOpen(false);
if (setIsClicked !== undefined) {
setIsClicked(false)
}
}}
onClose={handleClose}
PaperProps={{
style: {
backgroundColor: "black",
@@ -550,17 +563,15 @@ const WorkflowTemplatePopup = (props) => {
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 14,
top: 110,
right: 110,
color: "white",
}}
onClick={() => {
setModalOpen(false);
}}
onClick={handleClose}
>
<CloseIcon />
</IconButton>
<DialogContent style={{marginTop: 0, marginLeft: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, }}>
<DialogContent style={{marginTop: 0, marginLeft: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, marginTop: 20 }}>
<Typography variant="h4" style={{ fontSize: isMobile ? 20 : null}}>
<b>Configure Workflow</b>
</Typography>
@@ -28,7 +28,16 @@ import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import theme from "../theme.jsx";
const itemHeight = 24
export const getParentNodes = (workflow, action) => {
export const getParentNodes = (workflow, action, count) => {
if (count === undefined) {
count = 0
}
// 50 levels of parent nodes
if (count > 50) {
return []
}
if (action === undefined || action === null) {
return []
}
@@ -81,8 +90,7 @@ export const getParentNodes = (workflow, action) => {
continue;
}
// FIXME: This part is only handling first level,
// but needs to recurse
// FIXME: recursion
var incomingEdges = []
for (var branchkey in workflow.branches) {
const branch = workflow.branches[branchkey]
@@ -90,13 +98,19 @@ export const getParentNodes = (workflow, action) => {
continue
}
// Go up in the levels
// FIXME: Go up in the levels
// This somehow creates infinite recursion for now, so
// we are skipping it.
// This function is also not used for cytoscape recursion,
// so it doesn't matter much (yet)
/*
const parents = getParentNodes(workflow, {
id: branch.source_id,
})
}, count+1)
if (parents.length > 0) {
incomingEdges = incomingEdges.concat(parents)
}
*/
incomingEdges.push(branch)
}
@@ -215,8 +229,6 @@ const WorkflowValidationTimeline = (props) => {
}
}
//const parents = getParentNodes(workflow, action)
//console.log("PARENTS", key, parents)
if (parents !== undefined && parents !== null && parents.length > 0) {
const parentfound = parents.find((element) => element.id === startnodeId)
if (parentfound !== undefined) {
@@ -287,7 +299,7 @@ const WorkflowValidationTimeline = (props) => {
var scheduleNotStarted = false
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) {
console.log("Validation didn't run or get set for workflow. Why?")
//console.log("Validation didn't run or get set for workflow. Why?")
return null
}
@@ -386,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
@@ -549,8 +562,6 @@ const WorkflowValidationTimeline = (props) => {
nodecolor = green
branchcolor = green
}
var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3
if (nodecolor === green) {
@@ -597,6 +608,8 @@ const WorkflowValidationTimeline = (props) => {
const ballsize = 8
const topMargin = 20
console.log("CHIP: ", index, chipColor, chipBackground)
const chipStyle = {
height: 40,
minWidth: 125,
@@ -664,7 +677,6 @@ const WorkflowValidationTimeline = (props) => {
{founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
</Typography>
} placement="top">
<Chip label={`${appname}`} style={chipStyle} icon={
<Avatar
variant="round"
+81 -34
View File
@@ -7,11 +7,7 @@ import {
Tooltip,
TextField,
Grid,
Skeleton,
Dialog,
DialogTitle,
DialogContent,
Box,
Checkbox
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { Link } from "react-router-dom";
@@ -30,6 +26,8 @@ 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 [showOpenIdCred, setShowOpenIdCred] = React.useState(false);
const [showSamlCred, setShowSamlCred] = React.useState(false);
const [ssoEntrypoint, setSsoEntrypoint] = React.useState(
selectedOrganization.sso_config === undefined
? ""
@@ -393,7 +391,27 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
</span>
<Typography style={{ textAlign: "left", fontSize: 16, marginTop: 8, color: "rgba(158, 158, 158, 1)", fontWeight: 400 }}>
IdP URL for Shuffle OpenID: <Link to={`${globalUrl}/api/v1/login_openid`} target="_blank" style={{ color: "rgba(241, 241, 241, 1)", textDecoration: "none", fontSize: 16,}}>{`${globalUrl}/api/v1/login_openid`}</Link>
</Typography>
</Typography>
<div style={{ display: 'flex', marginTop: 10, }}>
<Typography
style={{
textAlign: "left",
fontSize: 16,
marginTop: 8,
color: "rgba(158, 158, 158, 1)",
fontWeight: 400,
}}
>
Show OpenID Credentials
</Typography>
<Checkbox
checked={showOpenIdCred}
onChange={(e) => setShowOpenIdCred(e.target.checked)}
name="showOpenIdCred"
color="primary"
style={{ color: "rgba(255, 255, 255, 1)", }}
/>
</div>
<Grid container style={{ marginTop: 8, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
@@ -407,30 +425,29 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The OpenID client ID from the identity provider"
value={openidClientId}
onChange={(e) => {
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: "white",
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
},
}}
/>
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
@@ -445,17 +462,19 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
type={showOpenIdCred ? "text" : "password"}
multiline={true}
rows={2}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
value={openidClientSecret}
value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""}
onChange={(e) => {
setOpenidClientSecret(e.target.value);
}}
}}
onFocus={(e) => setShowOpenIdCred(true)}
onBlur={(e) => setShowOpenIdCred(false)}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
@@ -485,17 +504,19 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
type={showOpenIdCred ? "text" : "password"}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID authorization URL (usually ends with /authorize)"
value={openidAuthorization}
value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""}
onChange={(e) => {
setOpenidAuthorization(e.target.value)
}}
}}
onFocus={(e) => setShowOpenIdCred(true)}
onBlur={(e) => setShowOpenIdCred(false)}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
@@ -523,17 +544,19 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
type={showOpenIdCred ? "text" : "password"}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID token URL (usually ends with /token)"
value={openidToken}
value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""}
onChange={(e) => {
setOpenidToken(e.target.value)
}}
}}
onFocus={(e) => setShowOpenIdCred(true)}
onBlur={(e) => setShowOpenIdCred(false)}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
@@ -557,7 +580,27 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
<Typography variant="h4" style={{ textAlign: "left", color: "rgba(241, 241, 241, 1)", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 600, }}>SAML SSO (v1.1)</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: "rgba(241, 241, 241, 1)", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, color: "rgba(158, 158, 158, 1)" }} color="textSecondary">
IdP URL for Shuffle SAML/SSO: <Link to={`${globalUrl}/api/v1/login_sso`} target="_blank" style={{ color: "rgba(241, 241, 241, 1)", textDecoration: "none" }}>{`${globalUrl}/api/v1/login_sso`}</Link>
</Typography>
</Typography>
<div style={{ display: 'flex', marginTop: 10, }}>
<Typography
style={{
textAlign: "left",
fontSize: 16,
marginTop: 8,
color: "rgba(158, 158, 158, 1)",
fontWeight: 400,
}}
>
Show SAML Credentials
</Typography>
<Checkbox
checked={showSamlCred}
onChange={(e) => setShowSamlCred(e.target.checked)}
name="showSamlCred"
color="primary"
style={{ color: "rgba(255, 255, 255, 1)", }}
/>
</div>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
@@ -571,17 +614,19 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
type={showSamlCred ? "text" : "password"}
multiline={true}
rows={2}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The entrypoint URL from your provider"
value={ssoEntrypoint}
value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
}}
onFocus={(e) => setShowSamlCred(true)}
onBlur={(e) => setShowSamlCred(false)}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
@@ -609,14 +654,16 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
type={showSamlCred ? "text" : "password"}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The X509 certificate to use"
value={ssoCertificate}
value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""}
onFocus={(e) => setShowSamlCred(true)}
onBlur={(e) => setShowSamlCred(false)}
onChange={(e) => {
setSsoCertificate(e.target.value);
}}
@@ -646,4 +693,4 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
)
}
export default SSOTab
export default SSOTab
+12 -4
View File
@@ -1,14 +1,16 @@
import { createContext, useState, useEffect } from 'react';
import React, { createContext, useState, useEffect } from 'react';
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);
useEffect(() => {
if (currentLocation?.includes('/workflows/') && leftSideBarOpenByClick === true) {
@@ -18,6 +20,10 @@ export const AppContext =(props) => {
//Calculate window width
useEffect(() => {
if (serverside === true) {
return
}
const handleResize = () => {
setWindowWidth(window?.innerWidth);
};
@@ -32,6 +38,8 @@ export const AppContext =(props) => {
return (
<Context.Provider value={{
isDocSearchModalOpen,
setIsDocSearchModalOpen,
searchBarModalOpen,
setSearchBarModalOpen,
leftSideBarOpenByClick,
+32
View File
@@ -157,6 +157,38 @@ const data = [
"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: {
+45 -13
View File
@@ -81,8 +81,13 @@ 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,
fontWeightMedium: 500,
fontWeightSemiBold: 600,
fontWeightBold: 700,
h1: {
fontSize: 40,
},
@@ -104,18 +109,45 @@ const theme = createTheme(adaptV4Theme({
},
},
MuiCssBaseline: {
MuiCssBaseline: {
styleOverrides: `
@font-face {
font-family: 'roboto';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('roboto'), local('roboto'), format('truetype');
unicodeRange: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF;
}
`,
},
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');
}
`,
},
},
},
}));
+18 -10
View File
@@ -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
@@ -70,13 +71,16 @@ const AdminAccount = (props) => {
setLoginInfo(responseJson["reason"]);
} 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")
});
};
@@ -108,13 +112,17 @@ const AdminAccount = (props) => {
setLoginInfo(responseJson["reason"]);
} else {
setLoginInfo("Successful register :)");
window.location.pathname = "/login";
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")
});
};
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -663,7 +663,7 @@ const ApiExplorerWrapper = (props) => {
}
}
} else if (validate.result.status === 404) {
toast.error("Page not found. Please try a different URL.")
//toast.error("Page not found. Please try a different URL.")
} else if (validate.result.error !== undefined && validate.result.error !== null && validate.result.error.length > 0) {
if (validate.result.error.toLowerCase().includes("max retries")) {
toast.error("Are you sure the URL is correct? It seems like the server is not responding.")
+5 -5
View File
@@ -281,7 +281,7 @@ export const appCategories = [
"name": "AI",
"color": "#FFC107",
"icon": "AI",
"action_labels": ["Answer Question", "Run Action"],
"action_labels": ["Answer Question", "Run Action", "Run LLM",],
},
{
"name": "Other",
@@ -2657,12 +2657,12 @@ const AppCreator = (defaultprops) => {
if (responseJson.reason !== undefined) {
setErrorCode(responseJson.reason);
if (responseJson.extra === undefined && responseJson.extra === null) {
toast("Failed to verify: " + responseJson.reason);
}
toast.error("Failed to build: " + responseJson.reason, {
autoClose: 10000
})
}
} 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}`;
+5 -6
View File
@@ -2906,7 +2906,7 @@ const AppExplorer = (props) => {
const deduplicateByName = (array) => {
const uniqueNames = {};
return array.filter(item => {
if (!item.hasOwnProperty('name') || !item.name.length) {
if (!item?.hasOwnProperty('name') || !item?.name?.length) {
return true
}
if (!uniqueNames[item.name]) {
@@ -3866,13 +3866,13 @@ const AppExplorer = (props) => {
}
}}
>
<DialogTitle style={{ marginBottom: 0 }}>
<DialogTitle style={{ marginBottom: 0, padding: 50, paddingBottom: 0, }}>
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.9)" }}>
Are you sure you want to PUBLISH this app?
</div>
</DialogTitle>
<DialogContent
style={{ color: "rgba(255,255,255,0.65)", textAlign: "center" }}
style={{ color: "rgba(255,255,255,0.65)", textAlign: "center", padding: 50, }}
>
<div>
<Typography variant="body1" style={{ marginBottom: 20 }}>
@@ -4120,7 +4120,7 @@ const AppExplorer = (props) => {
</Button>
)}
{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 ?
<Tooltip title="Edit App" placement="top">
<Button
variant="outlined"
@@ -4167,9 +4167,9 @@ const AppExplorer = (props) => {
Explore API
</Button>
</a>
{(appdata?.owner === userdata?.id) ? (
<Select
value={sharingConfiguration}
disabled={!isCloud}
onChange={(event) => {
if ((appdata.owner !== userdata.id) && (!userdata.support)) {
toast("You're not the owner of this app. You can't update it.");
@@ -4222,7 +4222,6 @@ const AppExplorer = (props) => {
);
})}
</Select>
): null }
</div>
) : (
<a
+7 -4
View File
@@ -176,16 +176,18 @@ export const GetParsedPaths = (inputdata, basekey) => {
}
if (typeof inputdata !== "object") {
return parsedValues;
return parsedValues
}
for (const [key, value] of Object.entries(inputdata)) {
for (var [key, value] of Object.entries(inputdata)) {
key = key.replaceAll(" ", "_")
// Check if loop or JSON
const extra = basekey.length > 0 ? splitkey : "";
const basekeyname = `${basekey
.slice(1, basekey.length)
.split(".")
.join(splitkey)}${extra}${key}`;
.join(splitkey)}${extra}${key}`
// Handle direct loop!
if (!isNaN(key) && basekey === "") {
@@ -205,7 +207,8 @@ export const GetParsedPaths = (inputdata, basekey) => {
type: "list",
name: `${splitkey}list`,
autocomplete: `${basekey.replaceAll(" ", "_")}.#`,
});
})
const returnValues = GetParsedPaths(value, `${basekey}.#`);
for (var subkey in returnValues) {
parsedValues.push(returnValues[subkey]);
+39 -20
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext, useCallback, memo, useMemo, useRef } from "react";
import theme 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,
@@ -49,15 +50,14 @@ const searchClient = algoliasearch(
);
// AppCard Component
const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, deactivatedIndexes, currTab, handleAppClick, leftSideBarOpenByClick, userdata, fetchApps }) => {
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}`;
//const appUrl = isCloud ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`;
const appUrl = `/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)
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 +71,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 (
<Grid item xs={12} key={index}>
@@ -133,6 +133,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
margin: "12px 0",
fontFamily: theme?.typography?.fontFamily,
}}>
<div style={{
display: 'flex',
flexDirection: 'row',
@@ -144,6 +145,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
maxWidth: "90%",
gap: 8
}}>
<div style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -152,6 +154,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
}}>
{data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
</div>
</div>
<div style={{
overflow: "hidden",
@@ -162,6 +165,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
}}>
{data.categories ? data.categories.join(", ") : "NA"}
</div>
<div style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -187,6 +191,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
</span>
))}
</div>
{/* Deactivate button */}
{(currTab === 0 || currTab === 1) && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && (
<div style={{
@@ -222,7 +227,9 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
</button>
)
}
<Button
disabled={data?.reference_org === userdata?.active_org?.id}
className="deactivate-button"
sx={{
width: 110,
@@ -244,7 +251,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
event.preventDefault();
event.stopPropagation();
const url = `${globalUrl}/api/v1/apps/${data.id}/deactivate`;
toast("Deactivating app. Please wait...");
//toast("Deactivating app. Please wait...");
fetch(url, {
method: 'GET',
headers: {
@@ -256,11 +263,14 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success === false) {
toast.error(responseJson.reason);
if (responseJson?.reason !== undefined && responseJson?.reason !== null && responseJson?.reason !== "") {
toast.error(responseJson.reason);
} else {
toast.error("Failed to deactivate app. Please try again later.")
}
} else {
toast.success("App Deactivated Successfully.");
fetchApps();
}
toast.success("App deactivated successfully. Will take effect on refresh..")
}
})
.catch(error => {
console.log("app error: ", error.toString());
@@ -501,7 +511,6 @@ const Hits = ({
fontFamily: theme?.typography?.fontFamily
}}
onClick={() => {
console.log("App modal", data)
handleAppClick(data);
}}
>
@@ -582,7 +591,7 @@ const Hits = ({
<div style={{
flex: 1
}}>
{hoverEffect === index && isCloud ? (
{hoverEffect === index ? (
<div>
{data.tags && (
<Tooltip
@@ -629,6 +638,7 @@ const Hits = ({
</div>
)}
</div>
<div style={{
display: 'flex',
justifyContent: 'flex-end',
@@ -638,7 +648,7 @@ const Hits = ({
}}>
{hoverEffect === index && (
<div>
{allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? (
{allActivatedAppIds && allActivatedAppIds?.includes(data.objectID) ? (
<Button
style={{
width: 110,
@@ -657,7 +667,9 @@ const Hits = ({
}}>
Deactivate
</Button>
) : (
<Button
style={{
backgroundColor: "#FF8544",
@@ -753,7 +765,7 @@ const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
<TextField
fullWidth
variant="outlined"
placeholder="Search for apps"
placeholder="Search from 2500+ public apps"
value={localQuery}
id="shuffle_search_field"
inputRef={inputRef}
@@ -1287,6 +1299,8 @@ const Apps2 = (props) => {
//console.log("Failed to get apps from localstorage: ", e)
}
setIsLoading(true);
fetch(globalUrl + "/api/v1/apps", {
method: "GET",
headers: {
@@ -1860,8 +1874,6 @@ const Apps2 = (props) => {
color: "#FF8544"
}
console.log("User", userdata)
return (
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: "#1A1A1A", fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
@@ -1998,7 +2010,7 @@ const Apps2 = (props) => {
}}
/>
<Tab
label="Discover Apps"
label="Discover Public Apps"
style={{
...tabStyle,
marginRight: 0,
@@ -2017,7 +2029,7 @@ const Apps2 = (props) => {
<TextField
fullWidth
variant="outlined"
placeholder="Search for apps"
placeholder={currTab === 1 ? "Search your apps" : "Search org apps"}
disabled={!isLoggedIn}
value={searchQuery}
id="shuffle_search_field"
@@ -2247,6 +2259,10 @@ const Apps2 = (props) => {
leftSideBarOpenByClick={leftSideBarOpenByClick}
userdata={userdata}
fetchApps={fetchApps}
setUserApps={setUserApps}
appsToShow={appsToShow}
setAppsToShow={setAppsToShow}
/>
))}
</div>
@@ -2296,6 +2312,9 @@ const Apps2 = (props) => {
<AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} deactivatedIndexes={deactivatedIndexes} currTab={currTab} userdata={userdata}
handleAppClick={handleAppClick} leftSideBarOpenByClick={leftSideBarOpenByClick}
fetchApps={fetchApps}
setUserApps={setUserApps}
appsToShow={appsToShow}
setAppsToShow={setAppsToShow}
/>
))}
</div>
+5 -4
View File
@@ -6,9 +6,6 @@ import classNames from "classnames";
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
// react plugin used to create charts
//import { Line, Bar } from "react-chartjs-2";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import Draggable from "react-draggable";
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
@@ -311,9 +308,13 @@ const RadialChart = ({keys, setSelectedCategory}) => {
//axis={<RadialAxis type="category" />}
}
// 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)
}
+450 -64
View File
@@ -5,7 +5,7 @@ import theme 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,26 @@ import {
List,
Collapse,
ListItemButton,
ListItemText
ListItemText,
Dialog,
DialogTitle,
Box,
DialogContent,
InputAdornment
} 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 +61,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 +163,34 @@ 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 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 +200,13 @@ export const Img = (props) => {
}
return(
<img
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: width, maxWidth: width, margin: "auto", marginTop: 10, marginBottom: 10, }}
alt={props.alt}
src={props.src}
/>
<div style={{width: "100%", textAlign: "center", }}>
<img
style={isArticlePage ? articleImageStyle : docsImageStyle}
alt={props.alt}
src={props.src}
/>
</div>
)
}
@@ -251,8 +287,12 @@ 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();
// Quickfix for react router 5 -> 6
const params = useParams();
//var props = JSON.parse(JSON.stringify(defaultprops))
@@ -283,16 +323,40 @@ 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({})
var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false;
const searchFieldRef = useRef(null);
useEffect(() => {
//if (params["key"] === undefined) {
// navigate("/docs/about")
// return
//}
}, [])
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
}
if (location.pathname.includes("/docs/")) {
if (propkey === "cookie_policy" || propkey === "compliance" || propkey === "privacy_policy" || propkey === "terms_of_service") {
navigate(`/legal/${propkey}`)
}
}
}, [location]);
useEffect(() => {
return () => {
setIsDocSearchModalOpen(false);
setSearchBarModalOpen(false);
};
}, []);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
}
@@ -422,6 +486,71 @@ const Docs = (defaultprops) => {
return headings;
};
const modalView = (
<Dialog
open={searchBarModalOpen && isDocSearchModalOpen}
onClose={() => {
setSearchBarModalOpen(false);
if (searchFieldRef.current) {
searchFieldRef.current.blur();
}
}}
PaperProps={{
style: {
color: "white",
minWidth: 750,
minHeight: "180px",
maxHeight: "85vh",
borderRadius: 16,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #000000)",
boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)",
position: "fixed",
top: "70px",
left: "50%",
transform: "translateX(-50%)",
},
}}
sx={{
zIndex: 50005,
'& .MuiBackdrop-root': {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", px: 2, pr: 3, pt: 2 }}>
<DialogTitle
sx={{
color: "var(--Paragraph-text, #C8C8C8)",
p: 0,
m: 0,
ml: 1.5,
fontFamily: theme.typography.fontFamily,
}}
>
Search for Documentation
</DialogTitle>
<IconButton
onClick={() => setSearchBarModalOpen(false)}
sx={{
color: 'white',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)'
}
}}
>
<CloseIcon />
</IconButton>
</Box>
<DialogContent>
<Box sx={{ pt: 3 }}>
<SearchBox globalUrl={globalUrl} serverside={serverside} userdata={userdata} />
</Box>
</DialogContent>
<Divider sx={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}/>
</Dialog>
);
// extract TOC from actual markdown
const tocvalue = (markdown) => {
@@ -460,8 +589,13 @@ const Docs = (defaultprops) => {
}
const SidebarPaperStyle = {
backgroundColor: "rgb(26,26,26)",
backgroundColor: isArticlePage ? "transparent" : "rgb(26,26,26)",
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,
@@ -527,7 +661,7 @@ const Docs = (defaultprops) => {
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette?.borderRadius,
marginBottom: 25,
marginBottom: isArticlePage ? 25 : 30,
display: "flex",
}}
>
@@ -620,7 +754,7 @@ const Docs = (defaultprops) => {
style={{
width: "90%",
marginTop: 60,
marginBottom: 20,
marginBottom: isArticlePage ? 40 : 20,
backgroundColor: theme.palette.inputColor,
}}
/>
@@ -630,7 +764,21 @@ const Docs = (defaultprops) => {
marginBlock: "0.85em", alignItems: "center"
}}>
{element}
<Link to={`#${id}`} style={{
<Link to={`#${id}`}
onClick={(e) => {
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",
@@ -638,8 +786,7 @@ const Docs = (defaultprops) => {
<LinkIcon />
</Link>
</div>
{extraInfo}
{isArticlePage ? (userdata?.support ? extraInfo : "") : extraInfo}
</Typography>
)
}
@@ -647,21 +794,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 +852,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 +880,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,18 +906,32 @@ 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 <img> tags and translate them into ![]() format
const imgRegex = /<img.*?src="(.*?)"/g;
const tocRegex = /^## Table of contents[\s\S]*?(?=^## )|^## Table of contents[\s\S]*$/gm;
const newdata = responseJson.reason.replace(imgRegex, '![]($1)').replace(tocRegex, "");
setData(newdata);
if (docId === undefined) {
document.title = "Shuffle automation documentation";
document.title = isArticlePage ? "Shuffle Articles introduction" : "Shuffle documentation";
} else {
document.title = "Shuffle " + docId.replace("_", " ") + " documentation";
document.title = "Shuffle " + docId.replace("_", " ") + (isArticlePage ? " articles" : " documentation");
}
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found") && !isArticlePage) {
navigate("/docs")
return
}
@@ -747,6 +961,11 @@ const Docs = (defaultprops) => {
.catch((error) => { });
};
const handleResetCache = () => {
fetchDocList(true);
toast("Cache has been reset");
}
if (firstrequest) {
setFirstrequest(false);
if (!serverside) {
@@ -769,11 +988,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,7 +1019,7 @@ 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 = {
@@ -793,7 +1030,7 @@ const Docs = (defaultprops) => {
maxWidth: "100%",
minWidth: "100%",
overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1rem",
fontSize: isMobile ? "1.3rem" : "1.1rem",
};
const alertNote = {
@@ -940,20 +1177,92 @@ const Docs = (defaultprops) => {
}
const activeHrefStyleToc2 = {
...hrefStyleToc2,
color: "#f86a3e",
};
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 : (
<div style={Body}>
<div style={SideBar}>
<div style={!isArticlePage ? SideBar : (sidebarOpen ? SideBar : collapsedSideBar)}>
<Paper style={SidebarPaperStyle}>
<List style={{ listStyle: "none", paddingLeft: "0" }}>
{modalView}
{
!isArticlePage && (
<TextField
variant="outlined"
placeholder="Search Docs..."
size="small"
onClick={() => {
setIsDocSearchModalOpen(true)
setSearchBarModalOpen(true)
}}
inputRef={searchFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon style={{ color: "white" }} />
</InputAdornment>
),
style: {
backgroundColor: "#212121",
borderRadius: 4,
color: "white",
},
}}
style={{
marginTop: 10,
marginLeft: 10,
borderRadius: 4,
width: !isLoggedIn ? "14%" : !leftSideBarOpenByClick ? "12%" : "11%",
maxWidth: "285px",
position: "fixed",
zIndex: 1100,
}}
inputProps={{
readOnly: true,
}}
/>
)
}
<List style={{
listStyle: "none",
paddingLeft: "0",
paddingTop: !isArticlePage ? "60px" : undefined,
paddingBottom: !isArticlePage ? "30px" : undefined,
display: isArticlePage ? sidebarOpen ? "block" : "none" : undefined,
opacity: isArticlePage ? sidebarOpen ? 1 : 0 : undefined,
transition: isArticlePage ? "opacity 0.3s ease" : undefined,
height: isArticlePage ? "90%" : undefined,
}}>
{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 +1274,62 @@ const Docs = (defaultprops) => {
<ListItemButton
component={Link}
key={index}
style={hrefStyle}
style={{
...hrefStyle,
...(itemMatching ? activeListItemStyle : {}),
}}
to={path}
>
<ListItemText
style={{ color: itemMatching ? "#f86a3e" : "inherit" }}
variant="body1"
>
{newname}
</ListItemText>
style={{
color: itemMatching ? "#f86a3e" : "inherit",
flex: 1,
}}
primary={
<Typography
variant="body1"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{newname}
</Typography>
}
/>
<KeyboardArrowRightIcon
style={{
color: itemMatching ? "#f86a3e" : "inherit",
fontSize: "1.1rem",
marginRight: "-6px",
}}
/>
</ListItemButton>
</li>
);
})}
</List>
{
isArticlePage && (
<IconButton
onClick={toggleSidebar}
style={sideBarToggleButton}
>
{sidebarOpen ? <ChevronLeft /> : <ChevronRight />}
</IconButton>
)
}
</Paper>
</div>
<div style={{ width: "60%", margin: "30px 0px 30px 30px", overflow: "hidden", paddingRight: 50, paddingLeft: 40 }}>
<div style={{
width: isArticlePage ? sidebarOpen ? "60%" : "70%" : "60%",
margin: isArticlePage ? sidebarOpen ? "0 0px 30px 30px" : "0 0px 30px 60px" : "30px 0px 30px 30px",
overflow: "hidden",
paddingRight: 50,
paddingLeft: 40,
transition: "width 0.3s ease, margin 0.3s ease",
}}>
{props.match.params.key === undefined ?
mainpageInfo
:
@@ -1003,12 +1351,26 @@ const Docs = (defaultprops) => {
}
</div>
<div style={IndexBar}>
{userdata?.support && isArticlePage && (
<Button
variant="contained"
color="secondary"
onClick={handleResetCache}
style={{marginBottom: "15px"}}
>
Reset Cache
</Button>
)}
{tocLines.length > 0 ?
(
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px" }}>Table of Content</h4>
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px" }}>Table Of Content</h4>
) : null}
<nav>
<div
style={{
overflowY: "auto",
maxHeight: "90vh",
}}>
{tocLines.map((data, index) => {
return (
<div className="toc"
@@ -1019,12 +1381,15 @@ const Docs = (defaultprops) => {
style={{
fontSize: "14px",
fontWeight: 400,
padding: "4px 8px 4px 8px",
padding: isArticlePage ? "4px 0" : "4px 8px 4px 8px",
paddingLeft: isArticlePage ? "0" : "8px",
paddingRight: isArticlePage ? "0" : "8px",
lineHeight: "20px",
color: activeId === data.id ? "#f86a3e" : "rgba(255,255,255,0.6)",
color: activeId === data.id ? "#f86a3e" : "inherit",
}}
onClick={(e) => {
handleCollapse(index)
setActiveId(data.id)
}}
>
{data.title}
@@ -1041,8 +1406,12 @@ const Docs = (defaultprops) => {
return (
<ListItemButton
key={i}
style={hrefStyleToc2}
style={activeSubItem === d.id ? activeHrefStyleToc2 : hrefStyleToc2}
href={`#${d.id}`}
onClick={(e) => {
// e.preventDefault()
setActiveSubItem(d.id)
}}
>
{d.title}
</ListItemButton>
@@ -1055,7 +1424,7 @@ const Docs = (defaultprops) => {
)
})}
</nav>
</div>
</div >
</div >
@@ -1083,7 +1452,7 @@ const Docs = (defaultprops) => {
color="primary"
onClick={handleClick}
>
<div style={{ color: "white" }}>More docs</div>
<div style={{ color: "white" }}>More {isArticlePage ? "articles" : "docs"}</div>
</Button>
<Menu
id="simple-menu"
@@ -1099,7 +1468,7 @@ const Docs = (defaultprops) => {
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 +1477,8 @@ const Docs = (defaultprops) => {
key={index}
style={{ color: "white" }}
onClick={() => {
window.location.pathname = path;
navigate(path)
handleClose()
}}
>
{newname}
@@ -1140,8 +1510,22 @@ const Docs = (defaultprops) => {
<MenuItem
key={index}
style={{ color: "white" }}
component={Link}
to={`#${data.id}`}
onClick={() => {
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();
}}
>
<a href={`#${data.id}`} style={hrefStyle}>{data.title}</a>
{data.title}
</MenuItem>
)
})}
@@ -1178,7 +1562,7 @@ const Docs = (defaultprops) => {
color="primary"
onClick={handleClick}
>
<div style={{ color: "white" }}>More docs</div>
<div style={{ color: "white" }}>More {isArticlePage ? "articles" : "docs"}</div>
</Button>
</div>
);
@@ -1198,7 +1582,7 @@ export default Docs;
const DocsContent = memo(({postDataBrowser, postDataMobile}) => {
return(
<div>
<div style={{fontFamily: theme?.palette?.fontFamily}}>
<BrowserView>{postDataBrowser}</BrowserView>
<MobileView>{postDataMobile}</MobileView>
</div>
@@ -1210,12 +1594,14 @@ const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{
return (
<div style={{
marginTop: window.location.pathname.includes("/articles/") ? 50 : undefined,
paddingBottom: 50,
minHeight: 1000, zIndex: 1,
maxWidth: Math.min(!(isLoggedIn && isLoaded) ? 1920 : leftSideBarOpenByClick ? windowWidth - 300 : windowWidth - 200, 1920),
minWidth: isMobile ? null : (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 800 : 900 : null, margin: "auto",
position: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? "relative" : "static",
left: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? 120 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0,
marginLeft: windowWidth < 1920 ? leftSideBarOpenByClick && (isLoggedIn && isLoaded) ? 160 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0 : "auto", width: "100%",
marginLeft: windowWidth < 1920 ? leftSideBarOpenByClick && (isLoggedIn && isLoaded) ? window.location.pathname.includes("/articles/") ? 0 : 90 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0 : "auto", width: "100%",
transition: "left 0.3s ease-in-out, min-width 0.3s ease-in-out, max-width 0.3s ease-in-out, position 0.3s ease-in-out, margin 0.3s ease-in-out, margin-left 0.3s ease"
}}>
{children}
File diff suppressed because it is too large Load Diff
+548
View File
@@ -0,0 +1,548 @@
/* 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"]);
} 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 ? (<div><p onClick={setLoginCheck(false)}>Want to register? Click here.</p></div>) : (<div><p onClick={setLoginCheck(true)}>Go back to login? Click here.</p></div>);
var formtitle = register ? <div>Login</div> : <div>Register</div>;
const imgsize = 100;
const basedata = (
<div style={bodyDivStyle}>
<Paper
style={{
paddingLeft: "30px",
paddingRight: "30px",
paddingBottom: "30px",
paddingTop: "30px",
position: "relative",
backgroundColor: theme.palette.surfaceColor,
}}
>
<div
style={{
position: "absolute",
top: -imgsize / 2 - 10,
left: 250 - imgsize / 2,
height: imgsize,
width: imgsize,
}}
>
<img
src="images/Shuffle_logo.png"
style={{
height: imgsize + 10,
width: imgsize + 10,
border: "2px solid rgba(255,255,255,0.6)",
borderRadius: imgsize,
}}
/>
</div>
{loginViewLoading ? (
<div style={{ textAlign: "center", marginTop: 50 }}>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
Waiting for the Shuffle database to become available. This may
take up to two minutes.
</Typography>
{loginInfo === undefined ||
loginInfo === null ||
loginInfo.length === 0 ? null : (
<div style={{ marginTop: "10px" }}>Database Response: {loginInfo}</div>
)}
<CircularProgress color="secondary" style={{ color: "white" }} />
<Paper
style={{
paddingLeft: "30px",
paddingRight: "30px",
paddingBottom: "30px",
paddingTop: "30px",
position: "relative",
textAlign: "left",
marginTop: 15,
}}
>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>
Are you sure Shuffle is{" "}
<a
rel="norefferer"
target="_blank"
href="https://github.com/frikky/Shuffle/blob/master/.github/install-guide.md"
style={{ textDecoration: "none", color: "#f86a3e" }}
>
installed correctly
</a>
?
</b>
</Typography>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>1.</b> Make sure shuffle-database folder has correct access, and that you have a minimum of <b>2Gb of RAM available</b>:{" "}
<br />
<br />
sudo chown -R 1000:1000 shuffle-database
</Typography>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>2.</b> Disable memory swap on the host:
<br />
<br />
sudo swapoff -a
</Typography>
<Typography
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>3</b>. Restart the database:
<br />
<br />
sudo docker restart shuffle-opensearch
</Typography>
</Paper>
<Typography
variant="body2"
style={{ marginBottom: 10, color: "white", marginTop: 20 }}
>
Need help?{" "}
<a
rel="norefferer"
target="_blank"
href="https://discord.gg/B2CBzUm"
style={{ textDecoration: "none", color: "#f86a3e" }}
>
Join the Discord!
</a>
</Typography>
</div>
) : (
<form
onSubmit={onSubmit}
style={{ margin: "15px 15px 15px 15px", color: "white" }}
>
<h2>{formtitle}</h2>
Username
<div>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 5,
}}
autoFocus
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="username"
placeholder="username@example.com"
id="emailfield"
margin="normal"
variant="outlined"
onChange={onChangeUser}
/>
</div>
Password
<div>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 5,
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
id="outlined-password-input"
fullWidth={true}
type="password"
autoComplete="current-password"
placeholder="**********"
margin="normal"
variant="outlined"
onChange={onChangePass}
/>
</div>
{MFAField === true ? (
<div style={{ marginTop: 15 }}>
2-factor code
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 5,
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
id="outlined-password-input"
fullWidth={true}
type="text"
placeholder="6-digit code"
margin="normal"
variant="outlined"
onChange={(event) => {
setMFAValue(event.target.value);
}}
/>
</div>
) : null}
<div style={{ display: "flex", marginTop: "15px" }}>
<Button
color="primary"
variant="contained"
type="submit"
style={{ flex: "1" }}
disabled={!handleValidateForm() || loginLoading}
>
{loginLoading ? (
<CircularProgress
color="secondary"
style={{ color: "white" }}
/>
) : (
"SUBMIT"
)}
</Button>
</div>
<div style={{ marginTop: "10px" }}>{loginInfo}</div>
{ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? (
<div>
<Typography style={{ textAlign: "center" }}>Or</Typography>
<div style={{ textAlign: "center", margin: 10 }}>
<Button
fullWidth
id="sso_button"
color="secondary"
variant="outlined"
type="button"
style={{ flex: "1", marginTop: 5 }}
onClick={() => {
//console.log("CLICK SSO");
window.location.href = ssoUrl
//navigate(ssoUrl)
}}
>
Use SSO
</Button>
</div>
</div>
) : null}
</form>
)}
</Paper>
</div>
);
const loadedCheck = isLoaded ? <div>{basedata}</div> : <div></div>;
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 <div style={{paddingBottom: 150, }}>{loadedCheck}</div>;
};
export default LoginDialog;
+20
View File
@@ -77,6 +77,24 @@ const RunWorkflow = (defaultprops) => {
const [boxWidth, setBoxWidth] = React.useState(500)
const [inputQuestions, setInputQuestions] = React.useState([])
useEffect(() => {
if (workflow === undefined || workflow === null || Object.keys(workflow).length === 0) {
return
}
if (workflow.input_questions === undefined || workflow.input_questions === null) {
return
}
// Checks if it's a user input-node based or not
if ((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) {
} else {
setInputQuestions(workflow.input_questions)
setUpdate(Math.random())
}
}, [workflow])
const IframeWrapper = (props) => {
var propsCopy = JSON.parse(JSON.stringify(props))
propsCopy.width = 400
@@ -1179,6 +1197,7 @@ const RunWorkflow = (defaultprops) => {
<form onSubmit={(e) => {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 :
<div>
{/*
<img
alt={workflow.name}
src={image}
@@ -1193,6 +1212,7 @@ const RunWorkflow = (defaultprops) => {
left: 200,
}}
/>
*/}
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
{organization}
+191 -71
View File
@@ -23,6 +23,7 @@ import {
Chip,
Checkbox,
Fade,
Skeleton,
} from "@mui/material";
import {
@@ -140,12 +141,166 @@ const UsecaseListComponent = (props) => {
const [firstLoad, setFirstLoad] = useState(true)
const [apps, setApps] = useState([])
const [autoOpenUsecase, setAutoOpenUsecase] = useState(null);
const classes = useStyles();
let navigate = useNavigate();
const [mitreTags, setMitreTags] = useState([]);
// Add loading state
const [isLoading, setIsLoading] = useState(true);
// Add this useEffect to handle URL parameters on load
useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const selectedUsecase = params["selected_object"];
if (selectedUsecase && keys.length > 0) {
const usecaseName = selectedUsecase.toLowerCase().replaceAll("_", " ");
// Find the matching usecase in the keys
for (const category of keys) {
const foundUsecase = category.list.find(
usecase => usecase.name.toLowerCase().replaceAll("_", " ") === usecaseName
);
if (foundUsecase) {
setAutoOpenUsecase(foundUsecase);
// Wait for render then scroll
setTimeout(() => {
const element = document.getElementById(usecaseName);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
});
}
}, 1000);
break;
}
}
}
}, [keys]);
// Add useEffect to handle auto-opening
useEffect(() => {
if (autoOpenUsecase) {
getUsecase(autoOpenUsecase, 0, 0);
setAutoOpenUsecase(null);
}
}, [autoOpenUsecase]);
// Loading skeleton component
const LoadingSkeleton = () => (
<div style={{paddingTop: 75, minHeight: 1000, textAlign: "left"}}>
{/* Header skeleton */}
<Skeleton variant="text" width={200} height={40} sx={{ bgcolor: 'grey.800' }} />
<Skeleton variant="text" width="60%" height={24} sx={{ marginTop: 3, bgcolor: 'grey.800' }} />
{/* Apps selection skeleton */}
<Skeleton variant="text" width={150} height={24} sx={{ marginTop: 5, marginBottom: 10 }} />
<Paper style={{
height: 60,
width: "97.5%",
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette?.borderRadius || 5,
display: "flex",
padding: "0 25px",
}}>
{/* App icons skeleton */}
<div style={{flex: 10, display: "flex", gap: 25, alignItems: "center"}}>
{[1, 2, 3, 4, 5].map((app) => (
<Skeleton
key={app}
variant="circular"
width={40}
height={40}
sx={{ bgcolor: 'grey.800' }}
/>
))}
</div>
{/* Add more apps button skeleton */}
<Skeleton
variant="rectangular"
width={150}
height={40}
sx={{
marginTop: "10px",
borderRadius: 20,
bgcolor: 'grey.800'
}}
/>
</Paper>
{/* Usecase categories skeleton - 3 sections: Collect, Enrich, Detect */}
{[1, 2, 3,4,5].map((category) => (
<div key={category} style={{marginTop: category === 1 ? 20: 45}}>
{/* Category title with color indicator */}
<Typography variant="body1" style={{marginBottom: 15}}>
<Skeleton
variant="text"
width={200}
height={24}
sx={{
bgcolor: category === 1 ? '#f85a3e33' :
category === 2 ? '#ffb00d33' :
category === 3 ? '#2196f333' :
category === 4 ? '#4caf5033' :
category === 5 ? '#9c27b033' : '#00000033'
}}
/>
</Typography>
<Grid container spacing={1}>
{[1, 2, 3].map((item) => (
<Grid item xs={isMobile ? 12 : 4} key={item}>
<Paper
style={{
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette?.borderRadius || 5,
padding: "10px 20px",
height: 80,
display: "flex",
alignItems: "center",
gap: 15
}}
>
{/* App icons container */}
<div style={{display: "flex", gap: 5}}>
<Skeleton
variant="circular"
width={30}
height={30}
sx={{ bgcolor: 'grey.800' }}
/>
<Skeleton
variant="circular"
width={30}
height={30}
sx={{ bgcolor: 'grey.800' }}
/>
</div>
{/* Usecase title */}
<Skeleton
variant="text"
width="70%"
height={24}
sx={{ bgcolor: 'grey.800' }}
/>
</Paper>
</Grid>
))}
</Grid>
</div>
))}
</div>
);
const parseUsecase = (subcase, inputFramework) => {
var useFramework = frameworkData
@@ -187,6 +342,7 @@ const UsecaseListComponent = (props) => {
}, [frameworkData])
const loadApps = () => {
setIsLoading(true);
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
headers: {
@@ -220,9 +376,11 @@ const UsecaseListComponent = (props) => {
}
setApps(responseJson);
setIsLoading(false);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
setIsLoading(false);
})
}
@@ -231,15 +389,23 @@ const UsecaseListComponent = (props) => {
}, [])
if (keys === undefined || keys === null || keys.length === 0) {
return null
return <LoadingSkeleton />;
}
// Timeout 50ms to delay it slightly
const getUsecase = (subcase, index, subindex) => {
subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
// Update URL with selected usecase
const usecaseName = subcase.name.toLowerCase().replaceAll(" ", "_")
const newUrl = `?selected_object=${usecaseName}`
// Force URL update even if it's the same usecase
navigate(newUrl, { replace: true })
// Parse and fetch usecase data
subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, {
method: "GET",
@@ -415,6 +581,19 @@ const UsecaseListComponent = (props) => {
}
}
// Then, add a cleanup function for URL params when drawer closes
const handleUsecaseClose = () => {
// Remove the selected_object parameter from URL
navigate("/usecases", { replace: true })
// Reset relevant state
setInputUsecase({})
setExpandedIndex(-1)
setExpandedItem(-1)
setFirstLoad(false)
setSelectedWorkflows([])
}
return (
<div style={{paddingTop: 75, minHeight: 1000, textAlign: "left",}}>
<Typography variant="h4" style={{color: "white", }}>
@@ -645,10 +824,10 @@ const UsecaseListComponent = (props) => {
return (
<Grid id={fixedName} item xs={isMobile ? 12 : 4} key={subindex} style={{}} onClick={() => {
if (fixedName === "reporting") {
getUsecase(subcase, index, subindex)
return
}
// if (fixedName === "reporting") {
// getUsecase(subcase, index, subindex)
// return
// }
//setSelectedWorkflows([])
if (selectedItem) {
@@ -678,6 +857,8 @@ const UsecaseListComponent = (props) => {
workflowBuilt={workflowBuilt}
inputWorkflowId={workflowBuilt}
usecaseDetails={usecaseDetails}
isModalOpenDefault={autoOpenUsecase?.name === subcase.name}
onClose={handleUsecaseClose}
/>
</Grid>
@@ -717,6 +898,7 @@ const Usecases2 = (props) => {
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (selectedUsecaseCategory.length === 0) {
@@ -729,71 +911,8 @@ const Usecases2 = (props) => {
}
}, [selectedUsecaseCategory])
const checkSelectedParams = () => {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname;
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const foundQuery = params["selected"]
if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem)
}
const baseItem = document.getElementById("reporting")
if (baseItem !== undefined && baseItem !== null) {
baseItem.click()
// Find close window button -> go to top
const foundButton = document.getElementById("close_selection")
if (foundButton !== undefined && foundButton !== null) {
foundButton.click()
}
// Scroll back to top
window.scrollTo(0, 0)
}
const foundQuery2 = params["selected_object"]
if (foundQuery2 !== null && foundQuery2 !== undefined) {
// Take a random object, quickly click it, then go to this one
// Something is weird with loading apps without it
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
// Waiting a bit for it to render
setTimeout(() => {
const foundItem = document.getElementById(queryName)
if (foundItem !== undefined && foundItem !== null) {
foundItem.click()
// Scroll to it
setTimeout(() => {
foundItem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
})
}, 100)
} else {
//console.log("Couldn't find item with name ", queryName)
}
}, 1000)
}
}
useEffect(() => {
if (usecases.length > 0) {
//console.log(usecases)
checkSelectedParams()
}
}, [usecases])
const getFramework = () => {
setIsLoading(true);
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
@@ -831,6 +950,7 @@ const Usecases2 = (props) => {
})
.catch((error) => {
toast(error.toString());
setIsLoading(false);
})
}
+1
View File
@@ -447,6 +447,7 @@ const Welcome = (props) => {
}
navigate("/welcome?tab=2")
setActiveStep(1)
setShowWelcome(true)
}}>
<CardActionArea style={actionObject}>
+113 -101
View File
@@ -427,11 +427,15 @@ const chipStyle = {
color: "white",
};
export const collapseField = (field) => {
export const collapseField = (field, inputdata) => {
if (field === undefined || field === null) {
return true
}
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
return false
}
if (field.name === "headers" || field.name === "cookies") {
return true
}
@@ -451,44 +455,44 @@ export const collapseField = (field) => {
}
export const validateJson = (showResult) => {
if (showResult === undefined || showResult === null) {
return {
valid: false,
result: "",
}
}
if (showResult === undefined || showResult === null) {
return {
valid: false,
result: "",
}
}
if (typeof showResult === 'string') {
showResult = showResult.split(" False").join(" false")
showResult = showResult.split(" True").join(" true")
if (typeof showResult === 'string') {
showResult = showResult.split(" False").join(" false")
showResult = showResult.split(" True").join(" true")
showResult.replaceAll("False,", "false,")
showResult.replaceAll("True,", "true,")
}
showResult.replaceAll("False,", "false,")
showResult.replaceAll("True,", "true,")
}
if (typeof showResult === "object" || typeof showResult === "array") {
return {
valid: true,
result: showResult,
}
}
if (typeof showResult === "object" || typeof showResult === "array") {
return {
valid: true,
result: showResult,
}
}
if (showResult[0] === "\"") {
return {
valid: false,
result: showResult,
}
}
if (showResult[0] === "\"") {
return {
valid: false,
result: showResult,
}
}
var jsonvalid = true
try {
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false
return {
valid: jsonvalid,
result: showResult,
};
return {
valid: jsonvalid,
result: showResult,
};
}
} catch (e) {
@@ -505,94 +509,94 @@ export const validateJson = (showResult) => {
var result = showResult;
try {
result = jsonvalid ? JSON.parse(showResult, { "storeAsString": true }) : showResult;
result = jsonvalid ? JSON.parse(showResult, {"storeAsString": true}) : showResult;
} catch (e) {
////console.log("Failed parsing JSON even though its valid: ", e)
jsonvalid = false;
}
if (jsonvalid === false) {
if (jsonvalid === false) {
if (typeof showResult === 'string') {
showResult = showResult.trim()
}
if (typeof showResult === 'string') {
showResult = showResult.trim()
}
try {
var newstr = showResult.replaceAll("'", '"')
try {
var newstr = showResult.replaceAll("'", '"')
// Basic workarounds for issues with Python Dicts -> JSON
if (newstr.includes(": None")) {
newstr = newstr.replaceAll(": None", ': null')
}
// Basic workarounds for issues with Python Dicts -> JSON
if (newstr.includes(": None")) {
newstr = newstr.replaceAll(": None", ': null')
}
if (newstr.includes("[\"{") && newstr.includes("}\"]")) {
newstr = newstr.replaceAll("[\"{", '[{')
newstr = newstr.replaceAll("}\"]", '}]')
}
if (newstr.includes("[\"{") && newstr.includes("}\"]")) {
newstr = newstr.replaceAll("[\"{", '[{')
newstr = newstr.replaceAll("}\"]", '}]')
}
if (newstr.includes("{\"[") && newstr.includes("]\"}")) {
newstr = newstr.replaceAll("{\"[", '[{')
newstr = newstr.replaceAll("]\"}", '}]')
}
if (newstr.includes("{\"[") && newstr.includes("]\"}")) {
newstr = newstr.replaceAll("{\"[", '[{')
newstr = newstr.replaceAll("]\"}", '}]')
}
result = JSON.parse(newstr)
jsonvalid = true
} catch (e) {
result = JSON.parse(newstr)
jsonvalid = true
} catch (e) {
//console.log("Failed parsing JSON even though its valid (2): ", e)
jsonvalid = false
}
}
//console.log("Failed parsing JSON even though its valid (2): ", e)
jsonvalid = false
}
}
if (jsonvalid && typeof result === "number") {
jsonvalid = false
}
if (jsonvalid && typeof result === "number") {
jsonvalid = false
}
// This is where we start recursing
if (jsonvalid) {
// Check fields if they can be parsed too
try {
for (const [key, value] of Object.entries(result)) {
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
//console.log("CHECKING STRING: ", value)
// This is where we start recursing
if (jsonvalid) {
// Check fields if they can be parsed too
try {
for (const [key, value] of Object.entries(result)) {
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
//console.log("CHECKING STRING: ", value)
const inside_result = validateJson(value)
if (inside_result.valid) {
//console.log("INSIDE RESULT: ", inside_result.result)
const inside_result = validateJson(value)
if (inside_result.valid) {
//console.log("INSIDE RESULT: ", inside_result.result)
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key] = newres
} else {
result[key] = inside_result.result
}
}
} else {
result[key] = newres
} else {
result[key] = inside_result.result
}
}
} else {
// Usually only reaches here if raw array > dict > value
if (typeof showResult !== "array") {
for (const [subkey, subvalue] of Object.entries(value)) {
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
const inside_result = validateJson(subvalue)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key][subkey] = newres
} else {
result[key][subkey] = inside_result.result
}
}
}
// Usually only reaches here if raw array > dict > value
if (typeof showResult !== "array") {
for (const [subkey, subvalue] of Object.entries(value)) {
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
const inside_result = validateJson(subvalue)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key][subkey] = newres
} else {
result[key][subkey] = inside_result.result
}
}
}
}
}
}
}
} catch (e) {
//console.log("Failed parsing inside json subvalues: ", e)
}
}
}
}
}
}
} catch (e) {
//console.log("Failed parsing inside json subvalues: ", e)
}
}
return {
valid: jsonvalid,
@@ -600,6 +604,7 @@ export const validateJson = (showResult) => {
};
};
//Custom hook for handling styling of the dropzone
const useDropzoneStyles = () => {
const { leftSideBarOpenByClick } = useContext(Context);
@@ -1841,9 +1846,13 @@ const Workflows = (props) => {
var parsedworkflows = [];
for (var key in newSubflows) {
if (key === data.id) {
continue
}
const foundWorkflow = workflows.find(
(workflow) => workflow.id === newSubflows[key]
);
)
if (foundWorkflow !== undefined && foundWorkflow !== null) {
parsedworkflows.push(foundWorkflow);
}
@@ -1854,7 +1863,7 @@ const Workflows = (props) => {
"Appending subflows during export: ",
parsedworkflows.length
);
data.subflows = parsedworkflows;
data.subflows = parsedworkflows
}
}
@@ -1937,7 +1946,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.`)
+35 -10
View File
@@ -724,6 +724,8 @@ const Workflows2 = (props) => {
const [apps, setApps] = React.useState([]);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
document.title = "Shuffle - Workflows";
useEffect(() => {
@@ -1758,9 +1760,17 @@ const Workflows2 = (props) => {
let exportFileDefaultName = data.name + ".json";
data["owner"] = "";
data["updated_by"] = "";
data["org"] = [];
data["org_id"] = "";
data["execution_org"] = {};
data["created"] = 0
data["due_date"] = 0
data["edited"] = 0
data["validation"] = {};
data["suborg_distribution"] = []
data["parentorg_workflow"] = ""
// These are backwards.. True = saved before. Very confuse.
data["previously_saved"] = false;
@@ -1935,6 +1945,10 @@ const Workflows2 = (props) => {
var parsedworkflows = [];
for (var key in newSubflows) {
if (key === data.id) {
continue
}
const foundWorkflow = workflows.find(
(workflow) => workflow.id === newSubflows[key]
);
@@ -2031,7 +2045,10 @@ const Workflows2 = (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. Do you have access?");
}
} else {
if (bulk !== true) {
toast(`Deleted workflow ${id}. Child Workflows in Suborgs were also removed.`)
@@ -2312,7 +2329,7 @@ const Workflows2 = (props) => {
key={"explore forms"}
>
<EditNoteIcon style={{ marginLeft: 0, marginRight: 8 }} />
{"Create Form"}
{"Edit Form"}
</MenuItem>
<Divider />
@@ -2362,7 +2379,6 @@ const Workflows2 = (props) => {
<MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => {
setDeleteModalOpen(true);
setSelectedWorkflowId(data.id);
@@ -2810,7 +2826,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" ?
<Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 45, right: 8, }}>
<div style={{ position: "absolute", top: 50, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2827,8 +2843,8 @@ const Workflows2 = (props) => {
: null}
{(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ?
<Tooltip title={`Explore more than ${data?.validation?.errors?.length} errors. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top">
<div style={{ position: "absolute", top: 45, right: 8, }}>
<Tooltip title={`Explore more than ${data?.validation?.errors?.length} notifications. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top">
<div style={{ position: "absolute", top: 85, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2847,8 +2863,8 @@ const Workflows2 = (props) => {
</IconButton>
</div>
</Tooltip>
: null}
</Grid>
</Paper>
</div>
@@ -3224,6 +3240,7 @@ const Workflows2 = (props) => {
</span>
</Tooltip>
}
<Tooltip
color="primary"
title="Amount of triggers"
@@ -4274,7 +4291,7 @@ const Workflows2 = (props) => {
display: "flex",
flexDirection: "column",
width: "100%",
maxWidth: "70%",
maxWidth: isSafari ? "100%" : "70%",
margin: "auto",
}}>
<Typography variant="h4" style={{ marginBottom: 20, paddingLeft: 15, textTransform: 'none', fontFamily: theme?.typography?.fontFamily }}>
@@ -4601,6 +4618,7 @@ const Workflows2 = (props) => {
variant="contained"
color="primary"
onClick={handleCreateWorkflow}
id="create_workflow_button"
style={{
borderRadius: 4,
flex: 0.8,
@@ -5045,8 +5063,9 @@ const Workflows2 = (props) => {
</iframe>
</Dialog>
//isLoaded && isLoggedIn && workflowDone ? (
const loadedCheck =
isLoaded && isLoggedIn && workflowDone ? (
workflowDone ? (
<div>
{/*
<ShepherdTour steps={newSteps} tourOptions={tourOptions}>
@@ -5113,8 +5132,14 @@ const Workflows2 = (props) => {
</div>
);
const safariStyle = {
transform: 'scale(0.7)',
transformOrigin: 'top',
width: '100%',
height: '100%',
}
// Maybe use gridview or something, idk
return <div style={{zoom: 0.7, }}>{loadedCheck}</div>;
return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>;
};
@@ -0,0 +1,3 @@
.DS_Store
*.tgz
charts/
@@ -0,0 +1,23 @@
# 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/
@@ -0,0 +1,11 @@
---
extends: default
rules:
line-length: disable
braces: disable
comments:
require-starting-space: true
ignore-shebangs: true
min-spaces-from-content: 1
@@ -0,0 +1,14 @@
apiVersion: v2
name: shuffle
description: A Helm chart for deploying Shuffle on Kubernetes
type: application
version: 0.0.0
appVersion: 0.0.0
dependencies:
- name: common
version: ^2.23.0
repository: oci://registry-1.docker.io/bitnamicharts
- name: opensearch
version: ^1.3.0
repository: oci://registry-1.docker.io/bitnamicharts
condition: opensearch.enabled
@@ -0,0 +1,607 @@
# Shuffle Helm chart
## Chart Template
The Bitnami Chart Template was used for creating this chart:
https://github.com/bitnami/charts/tree/7e44e64626f5b1fc6d56889cdfdeadc1f62c7cf1/template/CHART_NAME
Original license text:
```
Copyright Broadcom, Inc. All Rights Reserved.
SPDX-License-Identifier: APACHE-2.0
```
## Usage
```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
```
Make sure that no other application is deployed to the shuffle namespace, as shuffle deletes kubernetes resources in this namespace.
Only a single deployment of shuffle is supported per namespace.
## Uninstallation
```sh
# Uninstall shuffle via helm
helm uninstall shuffle --namespace shuffle
# Remove additional resources created by shuffle (such as workers and apps)
kubectl delete svc --namespace shuffle -l "app.kubernetes.io/managed-by in (shuffle-orborus,shuffle-worker)"
kubectl delete deploy --namespace shuffle -l "app.kubernetes.io/managed-by in (shuffle-orborus,shuffle-worker)"
```
## Secret Parameters
The helm chart was designed to not contain any secret data and does not allow configuring secret data using helm values.
Instead, secret values must be passed to services using `extraEnvVarsSecret` or `extraEnvVars`.
The secrets need to be manually created. It is possible to run this helm chart without specifying any secrets.
You will be prompted to create an admin user when visiting the shuffle dashboard for the first time.
Note that information will not be encrypted without specifying the `SHUFFLE_ENCRYPTION_MODIFIER` value.
### Mounting env variables into a service
After creating secrets which hold sensitive information, you can mount them as environment variables into a
service via the `extraEnvVarsSecret` or `extraEnvVars` values.
```yaml
backend:
# Use a single secret, which holds environment variables.
# Remember that the secret keys must exactly match the environment variable names.
extraEnvVarsSecret: shuffle-backend-env
# Or mount each value explicitly
extraEnvVars:
- name: SHUFFLE_DEFAULT_USERNAME
valueFrom:
secretKeyRef:
name: "shuffle-initial-user"
key: username
- name: SHUFFLE_DEFAULT_PASSWORD
valueFrom:
secretKeyRef:
name: "shuffle-initial-user"
key: password
- name: SHUFFLE_DEFAULT_APIKEY
valueFrom:
secretKeyRef:
name: "shuffle-initial-user"
key: apikey
- name: SHUFFLE_ENCRYPTION_MODIFIER
valueFrom:
secretKeyRef:
name: "shuffle-encryption"
key: modifier
```
### Backend
A list of environment variables containing secret values for the backend.
```yaml
# OpenSearch password
SHUFFLE_OPENSEARCH_PASSWORD: ""
# Basic auth credentials for downloading apps from git
SHUFFLE_DOWNLOAD_AUTH_USERNAME: ""
SHUFFLE_DOWNLOAD_AUTH_PASSWORD: ""
# Automatically create the initial admin user. Username and password have a min length of 3.
# If not set, you are prompted with an admin user creation dialog when visiting the shuffle frontend for the first time.
SHUFFLE_DEFAULT_USERNAME: admin
SHUFFLE_DEFAULT_PASSWORD: MySecretAdminPassword1234!
SHUFFLE_DEFAULT_APIKEY: "72E41083-A6F6-4A1B-8538-B06B577F47F0" # Shuffle uses uuid v4
# Encryption modifier. This HAS to be set to encrypt any authentication being used in Shuffle.
# This is put together with other relevant values to ensure multiple parts are needed to decrypt.
# If this key is lost or changed, you will have to reauthenticate all apps.
# The encryption modifier is added to encrypted values to prevent rainbow table attacks. It can be any random string.
SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier"
```
## Parameters
### Global parameters
| Name | Description | Value |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `global.imageRegistry` | Global Docker image registry | `""` |
| `global.imagePullSecrets` | Global Docker registry secret names as an array | `[]` |
| `global.defaultStorageClass` | Global default StorageClass for Persistent Volume(s) | `""` |
| `global.compatibility.openshift.adaptSecurityContext` | Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation) | `auto` |
| `global.compatibility.omitEmptySeLinuxOptions` | If set to true, removes the seLinuxOptions from the securityContexts when it is set to an empty object | `false` |
### Common parameters
| Name | Description | Value |
| ------------------------ | --------------------------------------------------------------------------------------- | --------------- |
| `kubeVersion` | Override Kubernetes version | `""` |
| `nameOverride` | String to partially override common.names.name | `""` |
| `fullnameOverride` | String to fully override common.names.fullname | `""` |
| `namespaceOverride` | String to fully override common.names.namespace | `""` |
| `commonLabels` | Labels to add to all deployed objects | `{}` |
| `commonAnnotations` | Annotations to add to all deployed objects | `{}` |
| `clusterDomain` | Kubernetes cluster domain name | `cluster.local` |
| `extraDeploy` | Array of extra objects to deploy with the release | `[]` |
| `diagnosticMode.enabled` | Enable diagnostic mode (all probes will be disabled and the command will be overridden) | `false` |
| `diagnosticMode.command` | Command to override all containers in the chart release | `["sleep"]` |
| `diagnosticMode.args` | Args to override all containers in the chart release | `["infinity"]` |
### Shared Shuffle Parameters
| Name | Description | Value |
| --------------------- | ------------------------------------------------------------- | --------------- |
| `shuffle.baseUrl` | The external base URL under which Shuffle is reachable. | `""` |
| `shuffle.org` | Default shuffle organization | `Shuffle` |
| `shuffle.appRegistry` | The registry from / to which shuffle apps are pulled / pushed | `""` |
| `shuffle.timezone` | The timezone used by Shuffle | `Europe/Berlin` |
### backend Parameters
| Name | Description | Value |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `backend.image.registry` | backend image registry | `ghcr.io` |
| `backend.image.repository` | backend image repository | `shuffle/shuffle-backend` |
| `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 | `[]` |
| `backend.replicaCount` | Number of backend replicas to deploy | `1` |
| `backend.containerPorts.http` | backend HTTP container port | `5001` |
| `backend.extraContainerPorts` | Optionally specify extra list of additional ports for backend containers | `[]` |
| `backend.livenessProbe.enabled` | Enable livenessProbe on backend containers | `false` |
| `backend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` |
| `backend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` |
| `backend.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` |
| `backend.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` |
| `backend.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` |
| `backend.readinessProbe.enabled` | Enable readinessProbe on backend containers | `false` |
| `backend.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` |
| `backend.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` |
| `backend.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` |
| `backend.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` |
| `backend.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` |
| `backend.startupProbe.enabled` | Enable startupProbe on backend containers | `false` |
| `backend.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` |
| `backend.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` |
| `backend.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` |
| `backend.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` |
| `backend.startupProbe.successThreshold` | Success threshold for startupProbe | `1` |
| `backend.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` |
| `backend.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` |
| `backend.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` |
| `backend.resourcesPreset` | Set backend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if backend.resources is set (backend.resources is recommended for production). | `small` |
| `backend.resources` | Set backend container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `backend.podSecurityContext.enabled` | Enable backend pods' Security Context | `true` |
| `backend.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for backend pods | `Always` |
| `backend.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for backend pods | `[]` |
| `backend.podSecurityContext.supplementalGroups` | Set filesystem extra groups for backend pods | `[]` |
| `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.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` |
| `backend.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in backend container' Security Context | `false` |
| `backend.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in backend container | `["ALL"]` |
| `backend.containerSecurityContext.seccompProfile.type` | Set seccomp profile in backend container | `RuntimeDefault` |
| `backend.command` | Override default backend container command (useful when using custom images) | `[]` |
| `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` | `""` |
| `backend.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `soft` |
| `backend.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `""` |
| `backend.nodeAffinityPreset.key` | Node label key to match. Ignored if `backend.affinity` is set | `""` |
| `backend.nodeAffinityPreset.values` | Node label values to match. Ignored if `backend.affinity` is set | `[]` |
| `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.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 | `""` |
| `backend.terminationGracePeriodSeconds` | Seconds backend pods need to terminate gracefully | `""` |
| `backend.lifecycleHooks` | for backend containers to automate configuration before or after startup | `{}` |
| `backend.extraEnvVars` | Array with extra environment variables to add to backend containers | `[]` |
| `backend.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for backend containers | `""` |
| `backend.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for backend containers | `""` |
| `backend.extraVolumes` | Optionally specify extra list of additional volumes for the backend pods | `[]` |
| `backend.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the backend containers | `[]` |
| `backend.sidecars` | Add additional sidecar containers to the backend pods | `[]` |
| `backend.initContainers` | Add additional init containers to the backend pods | `[]` |
| `backend.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` |
| `backend.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` |
| `backend.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `backend.pdb.minAvailable` and `backend.pdb.maxUnavailable` are empty. | `""` |
| `backend.autoscaling.vpa.enabled` | Enable VPA for backend pods | `false` |
| `backend.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` |
| `backend.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` |
| `backend.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` |
| `backend.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` |
| `backend.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` |
| `backend.autoscaling.hpa.enabled` | Enable HPA for backend pods | `false` |
| `backend.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` |
| `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.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) | `{}` |
| `backend.serviceAccount.automountServiceAccountToken` | Automount service account token for the backend service account | `true` |
| `backend.serviceAccount.imagePullSecrets` | Add image pull secrets to the backend service account | `[]` |
| `backend.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `backend.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `backend.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `backend.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `backend.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `backend.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
| `backend.cleanupSchedule` | The interval in seconds at which the cleanup job runs | `300` |
| `backend.openSearch.url` | The URL at which OpenSearch is available | `http://{{ .Release.Name }}-opensearch:9200` |
| `backend.openSearch.username` | The username that is used for authenticating with OpenSearch | `admin` |
| `backend.openSearch.certificateFile` | The path to a custom OpenSearch certificate file | `""` |
| `backend.openSearch.skipSSLVerify` | Skip SSL verification | `false` |
| `backend.openSearch.indexPrefix` | A prefix for OpenSearch indices | `""` |
| `backend.apps.downloadLocation` | The location to a git repository from which default appps are downloaded on startup. | `https://github.com/shuffle/python-apps` |
| `backend.apps.downloadBranch` | The branch from which apps should be downloaded on startup. | `master` |
| `backend.apps.forceUpdate` | Force an update of apps on startup. | `false` |
### frontend Parameters
| Name | Description | Value |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `frontend.image.registry` | frontend image registry | `ghcr.io` |
| `frontend.image.repository` | frontend image repository | `shuffle/shuffle-frontend` |
| `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 | `[]` |
| `frontend.replicaCount` | Number of frontend replicas to deploy | `1` |
| `frontend.containerPorts.http` | frontend HTTP container port | `80` |
| `frontend.containerPorts.https` | frontend HTTPS container port | `443` |
| `frontend.extraContainerPorts` | Optionally specify extra list of additional ports for frontend containers | `[]` |
| `frontend.livenessProbe.enabled` | Enable livenessProbe on frontend containers | `false` |
| `frontend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` |
| `frontend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` |
| `frontend.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` |
| `frontend.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` |
| `frontend.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` |
| `frontend.readinessProbe.enabled` | Enable readinessProbe on frontend containers | `false` |
| `frontend.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` |
| `frontend.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` |
| `frontend.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` |
| `frontend.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` |
| `frontend.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` |
| `frontend.startupProbe.enabled` | Enable startupProbe on frontend containers | `false` |
| `frontend.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` |
| `frontend.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` |
| `frontend.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` |
| `frontend.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` |
| `frontend.startupProbe.successThreshold` | Success threshold for startupProbe | `1` |
| `frontend.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` |
| `frontend.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` |
| `frontend.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` |
| `frontend.resourcesPreset` | Set frontend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if frontend.resources is set (frontend.resources is recommended for production). | `nano` |
| `frontend.resources` | Set frontend container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `frontend.podSecurityContext.enabled` | Enable frontend pods' Security Context | `false` |
| `frontend.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for frontend pods | `Always` |
| `frontend.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for frontend pods | `[]` |
| `frontend.podSecurityContext.supplementalGroups` | Set filesystem extra groups for frontend pods | `[]` |
| `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.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` |
| `frontend.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in frontend container' Security Context | `false` |
| `frontend.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in frontend container | `["ALL"]` |
| `frontend.containerSecurityContext.seccompProfile.type` | Set seccomp profile in frontend container | `RuntimeDefault` |
| `frontend.command` | Override default frontend container command (useful when using custom images) | `[]` |
| `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` | `""` |
| `frontend.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `soft` |
| `frontend.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `""` |
| `frontend.nodeAffinityPreset.key` | Node label key to match. Ignored if `frontend.affinity` is set | `""` |
| `frontend.nodeAffinityPreset.values` | Node label values to match. Ignored if `frontend.affinity` is set | `[]` |
| `frontend.affinity` | Affinity for frontend pods assignment | `{}` |
| `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 | `""` |
| `frontend.terminationGracePeriodSeconds` | Seconds frontend pods need to terminate gracefully | `""` |
| `frontend.lifecycleHooks` | for frontend containers to automate configuration before or after startup | `{}` |
| `frontend.extraEnvVars` | Array with extra environment variables to add to frontend containers | `[]` |
| `frontend.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for frontend containers | `""` |
| `frontend.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for frontend containers | `""` |
| `frontend.extraVolumes` | Optionally specify extra list of additional volumes for the frontend pods | `[]` |
| `frontend.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the frontend containers | `[]` |
| `frontend.sidecars` | Add additional sidecar containers to the frontend pods | `[]` |
| `frontend.initContainers` | Add additional init containers to the frontend pods | `[]` |
| `frontend.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` |
| `frontend.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` |
| `frontend.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `frontend.pdb.minAvailable` and `frontend.pdb.maxUnavailable` are empty. | `""` |
| `frontend.autoscaling.vpa.enabled` | Enable VPA for frontend pods | `false` |
| `frontend.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` |
| `frontend.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` |
| `frontend.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` |
| `frontend.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` |
| `frontend.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` |
| `frontend.autoscaling.hpa.enabled` | Enable HPA for frontend pods | `false` |
| `frontend.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` |
| `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.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) | `{}` |
| `frontend.serviceAccount.automountServiceAccountToken` | Automount service account token for the frontend service account | `true` |
| `frontend.serviceAccount.imagePullSecrets` | Add image pull secrets to the frontend service account | `[]` |
| `frontend.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `frontend.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `frontend.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `frontend.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `frontend.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
### orborus Parameters
| Name | Description | Value |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `orborus.image.registry` | orborus image registry | `ghcr.io` |
| `orborus.image.repository` | orborus image repository | `shuffle/shuffle-orborus` |
| `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` |
| `orborus.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` |
| `orborus.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` |
| `orborus.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` |
| `orborus.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` |
| `orborus.readinessProbe.enabled` | Enable readinessProbe on orborus containers | `false` |
| `orborus.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` |
| `orborus.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` |
| `orborus.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` |
| `orborus.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` |
| `orborus.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` |
| `orborus.startupProbe.enabled` | Enable startupProbe on orborus containers | `false` |
| `orborus.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` |
| `orborus.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` |
| `orborus.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` |
| `orborus.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` |
| `orborus.startupProbe.successThreshold` | Success threshold for startupProbe | `1` |
| `orborus.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` |
| `orborus.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` |
| `orborus.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` |
| `orborus.resourcesPreset` | Set orborus container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if orborus.resources is set (orborus.resources is recommended for production). | `nano` |
| `orborus.resources` | Set orborus container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `orborus.podSecurityContext.enabled` | Enable orborus pods' Security Context | `true` |
| `orborus.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for orborus pods | `Always` |
| `orborus.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for orborus pods | `[]` |
| `orborus.podSecurityContext.supplementalGroups` | Set filesystem extra groups for orborus pods | `[]` |
| `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.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` |
| `orborus.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in orborus container' Security Context | `false` |
| `orborus.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in orborus container | `["ALL"]` |
| `orborus.containerSecurityContext.seccompProfile.type` | Set seccomp profile in orborus container | `RuntimeDefault` |
| `orborus.command` | Override default orborus container command (useful when using custom images) | `[]` |
| `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` | `""` |
| `orborus.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `soft` |
| `orborus.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `""` |
| `orborus.nodeAffinityPreset.key` | Node label key to match. Ignored if `orborus.affinity` is set | `""` |
| `orborus.nodeAffinityPreset.values` | Node label values to match. Ignored if `orborus.affinity` is set | `[]` |
| `orborus.affinity` | Affinity for orborus pods assignment | `{}` |
| `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 | `""` |
| `orborus.terminationGracePeriodSeconds` | Seconds orborus pods need to terminate gracefully | `""` |
| `orborus.lifecycleHooks` | for orborus containers to automate configuration before or after startup | `{}` |
| `orborus.extraEnvVars` | Array with extra environment variables to add to orborus containers | `[]` |
| `orborus.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for orborus containers | `""` |
| `orborus.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for orborus containers | `""` |
| `orborus.extraVolumes` | Optionally specify extra list of additional volumes for the orborus pods | `[]` |
| `orborus.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the orborus containers | `[]` |
| `orborus.sidecars` | Add additional sidecar containers to the orborus pods | `[]` |
| `orborus.initContainers` | Add additional init containers to the orborus pods | `[]` |
| `orborus.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` |
| `orborus.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` |
| `orborus.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `orborus.pdb.minAvailable` and `orborus.pdb.maxUnavailable` are empty. | `""` |
| `orborus.autoscaling.vpa.enabled` | Enable VPA for orborus pods | `false` |
| `orborus.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` |
| `orborus.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` |
| `orborus.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` |
| `orborus.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` |
| `orborus.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` |
| `orborus.autoscaling.hpa.enabled` | Enable HPA for orborus pods | `false` |
| `orborus.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` |
| `orborus.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` |
| `orborus.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` |
| `orborus.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` |
| `orborus.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `orborus.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `orborus.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `orborus.serviceAccount.automountServiceAccountToken` | Automount service account token for the orborus service account | `true` |
| `orborus.serviceAccount.imagePullSecrets` | Add image pull secrets to the orborus service account | `[]` |
| `orborus.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `orborus.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `orborus.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `orborus.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `orborus.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `orborus.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
### 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) | `[]` |
### app Parameters
| Name | Description | Value |
| ------------------------------------------------- | ---------------------------------------------------------------------------------- | ------ |
| `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
| Name | Description | Value |
| -------------------------- | ----------------------------------------------------------------------------------------------------- | --------------- |
| `ingress.enabled` | Enable ingress record generation for frontend and backend | `false` |
| `ingress.pathType` | Ingress path type for the frontend path | `Prefix` |
| `ingress.backendPathType` | Ingress path type for the backend path | `Prefix` |
| `ingress.apiVersion` | Force Ingress API version (automatically detected if not set) | `""` |
| `ingress.hostname` | Default host for the ingress record | `shuffle.local` |
| `ingress.ingressClassName` | IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) | `nginx` |
| `ingress.path` | Ingress path for Shuffle frontend | `"/"` |
| `ingress.backendPath` | Ingress path for Shuffle backend | `"/api/"` |
| `ingress.annotations` | Additional annotations for the Ingress resource. | `{}` |
| `ingress.tls` | Enable TLS configuration for the host defined at `ingress.hostname` parameter | `false` |
| `ingress.selfSigned` | Create a TLS secret for this ingress record using self-signed certificates generated by Helm | `false` |
| `ingress.extraHosts` | An array with additional hostname(s) to be covered with the ingress record | `[]` |
| `ingress.extraPaths` | An array with additional arbitrary paths that may need to be added to the ingress under the main host | `[]` |
| `ingress.extraTls` | TLS configuration for additional hostname(s) to be covered with this ingress record | `[]` |
| `ingress.secrets` | Custom TLS certificates as secrets | `[]` |
| `ingress.extraRules` | Additional rules to be covered with this ingress record | `[]` |
### Istio Parameters
| Name | Description | Value |
| --------------------------------------- | ------------------------------------------------------------------------------- | ------------------------ |
| `istio.enabled` | Enable creation of an Istio Gateway and VirtualService for frontend and backend | `false` |
| `istio.apiVersion` | The istio apiVersion to use for Gateway and VirtualService resources | `networking.istio.io/v1` |
| `istio.hosts` | One or more hosts exposed by Istio | `[]` |
| `istio.gateway.annotations` | Additional annotations for the Gateway resource | `{}` |
| `istio.gateway.selector` | The selector matches the ingress gateway pod labels | `{ istio: ingress }` |
| `istio.gateway.http.enabled` | Enable HTTP server port 80 | `true` |
| `istio.gateway.http.httpsRedirect` | If set to true, a 301 redirect is send for all HTTP connections | `false` |
| `istio.gateway.https.enabled` | Enable HTTPS server on port 443 | `false` |
| `istio.gateway.https.tlsCredentialName` | The name of the secret that holds the TLS certs including the CA certificates. | `""` |
| `istio.gateway.https.tlsCipherSuites` | If specified, only support the specified cipher list. | `[]` |
| `istio.gateway.extraServers` | Additional servers for the Gateway resource | `[]` |
| `istio.virtualService.annotations` | Additional annotations for the VirtualService resource. | `{}` |
| `istio.virtualService.backendHeaders` | Header manipulation rules for backend traffic | `{}` |
| `istio.virtualService.frontendHeaders` | Header manipulation rules for frontend traffic | `{}` |
### Persistence Parameters
| Name | Description | Value |
| ------------------------------------- | ------------------------------------------------- | ------------------- |
| `persistence.enabled` | Enable persistence using Persistent Volume Claims | `true` |
| `persistence.apps.existingClaim` | Name of an existing PVC to use | `""` |
| `persistence.apps.storageClass` | PVC Storage Class for shuffle-apps volume | `""` |
| `persistence.apps.subPath` | The sub path used in the volume | `""` |
| `persistence.apps.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` |
| `persistence.apps.size` | The size of the volume | `5Gi` |
| `persistence.apps.annotations` | Annotations for the PVC | `{}` |
| `persistence.apps.selector` | Selector to match an existing Persistent Volume | `{}` |
| `persistence.appBuilder.storageClass` | PVC Storage Class for backend-apps-claim volume | `""` |
| `persistence.appBuilder.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` |
| `persistence.appBuilder.size` | The size of the volume | `5Gi` |
| `persistence.appBuilder.annotations` | Annotations for the PVC | `{}` |
| `persistence.appBuilder.selector` | Selector to match an existing Persistent Volume | `{}` |
| `persistence.files.existingClaim` | Name of an existing PVC to use | `""` |
| `persistence.files.storageClass` | PVC Storage Class for shuffle-files volume | `""` |
| `persistence.files.subPath` | The sub path used in the volume | `""` |
| `persistence.files.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` |
| `persistence.files.size` | The size of the volume | `5Gi` |
| `persistence.files.annotations` | Annotations for the PVC | `{}` |
| `persistence.files.selector` | Selector to match an existing Persistent Volume | `{}` |
### Init Container Parameters
| Name | Description | Value |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `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.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` |
| `volumePermissions.resources` | Set init container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `volumePermissions.containerSecurityContext.enabled` | Enabled init container' Security Context | `true` |
| `volumePermissions.containerSecurityContext.seLinuxOptions` | Set SELinux options in init container | `{}` |
| `volumePermissions.containerSecurityContext.runAsUser` | Set init container's Security Context runAsUser | `0` |
### OpenSearch Parameters
| Name | Description | Value |
| -------------------- | ----------------------------------------------------- | ------ |
| `opensearch.enabled` | Switch to enable or disable the opensearch helm chart | `true` |
### Vault Parameters
| Name | Description | Value |
| --------------- | -------------------------------------------------------------------------- | ----- |
| `vault.role` | Specify the Vault role, which should be used to get the secret from Vault. | `""` |
| `vault.secrets` | A list of VaultSecrets to create | `[]` |
### Other Parameters
@@ -0,0 +1,27 @@
CHART NAME: {{ .Chart.Name }}
CHART VERSION: {{ .Chart.Version }}
APP VERSION: {{ .Chart.AppVersion }}
** Please be patient while the chart is being deployed **
{{- if .Values.diagnosticMode.enabled }}
The chart has been deployed in diagnostic mode. All probes have been disabled and the command has been overwritten with:
command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 4 }}
args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 4 }}
Get the list of pods by executing:
kubectl get pods --namespace {{ include "common.names.namespace" . | quote }} -l app.kubernetes.io/instance={{ .Release.Name }}
Access the pod you want to debug by executing
kubectl exec --namespace {{ include "common.names.namespace" . | quote }} -ti <NAME OF THE POD> -- bash
{{- end }}
To access shuffle using port-forwarding:
1. Run `kubectl port-forward -n shuffle svc/shuffle-frontend 8080:http`
2. Visit http://localhost:8080 with your browser
@@ -0,0 +1,377 @@
{{/*
Return the common name for backend componentes
*/}}
{{- define "shuffle.backend.name" -}}
{{- printf "%s-backend" (include "common.names.fullname" .) | trunc 63 -}}
{{- end -}}
{{/*
Return the common name for frontend components
*/}}
{{- define "shuffle.frontend.name" -}}
{{- printf "%s-frontend" (include "common.names.fullname" .) | trunc 63 -}}
{{- end -}}
{{/*
Return the common name for orborus components
*/}}
{{- define "shuffle.orborus.name" -}}
{{- printf "%s-orborus" (include "common.names.fullname" .) | trunc 63 -}}
{{- end -}}
{{/*
Return the common name for worker components
*/}}
{{- define "shuffle.worker.name" -}}
{{- printf "%s-worker" (include "common.names.fullname" .) | trunc 63 -}}
{{- end -}}
{{/*
Return the common name for app components
*/}}
{{- define "shuffle.app.name" -}}
{{- printf "%s-app" (include "common.names.fullname" .) | trunc 63 -}}
{{- end -}}
{{/*
Return the common labels for backend components
The shuffle app builder requires the io.kompose.service=backend label to be set on the backend pod.
*/}}
{{- define "shuffle.backend.labels" -}}
{{- include "common.labels.standard" . }}
app.kubernetes.io/component: backend
io.kompose.service: backend
{{- end -}}
{{/*
Return the common labels for frontend components
*/}}
{{- define "shuffle.frontend.labels" -}}
{{- include "common.labels.standard" . }}
app.kubernetes.io/component: frontend
{{- end -}}
{{/*
Return the common labels for orborus components
*/}}
{{- define "shuffle.orborus.labels" -}}
{{- include "common.labels.standard" . }}
app.kubernetes.io/component: orborus
{{- end -}}
{{/*
Return the common labels for worker components
*/}}
{{- define "shuffle.worker.labels" -}}
{{- include "common.labels.standard" . }}
app.kubernetes.io/component: worker
{{- end -}}
{{/*
Return the common labels for app components
*/}}
{{- define "shuffle.app.labels" -}}
{{- include "common.labels.standard" . }}
app.kubernetes.io/component: app
{{- end -}}
{{/*
Return the match labels for backend components
*/}}
{{- define "shuffle.backend.matchLabels" -}}
{{- include "common.labels.matchLabels" . }}
app.kubernetes.io/component: backend
{{- end -}}
{{/*
Return the match labels for frontend components
*/}}
{{- define "shuffle.frontend.matchLabels" -}}
{{- include "common.labels.matchLabels" . }}
app.kubernetes.io/component: frontend
{{- end -}}
{{/*
Return the match labels for orborus components
*/}}
{{- define "shuffle.orborus.matchLabels" -}}
{{- include "common.labels.matchLabels" . }}
app.kubernetes.io/component: orborus
{{- end -}}
{{/*
Return the match labels for worker components
NOTE: This does not match the labels from shuffle.worker.labels, but the labels set by the orborus GoLang app.
*/}}
{{- define "shuffle.worker.matchLabels" -}}
app.kubernetes.io/name: shuffle-worker
{{- end -}}
{{/*
Return the match labels for app components
NOTE: This does not match the labels from shuffle.worker.labels, but the labels set by the orborus GoLang app.
*/}}
{{- define "shuffle.app.matchLabels" -}}
app.kubernetes.io/name: shuffle-app
{{- end -}}
{{/*
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 ) -}}
{{- end -}}
{{/*
Return the proper Shuffle backend image name
*/}}
{{- define "shuffle.backend.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.backend.image "global" .Values.global ) -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the backend pod
*/}}
{{- define "shuffle.backend.imagePullSecrets" -}}
{{- include "common.images.renderPullSecrets" (dict "images" (list .Values.backend.image) "context" $) -}}
{{- end -}}
{{/*
Return the proper Shuffle frontend image name
*/}}
{{- define "shuffle.frontend.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.frontend.image "global" .Values.global ) -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the frontend pod
*/}}
{{- define "shuffle.frontend.imagePullSecrets" -}}
{{- include "common.images.renderPullSecrets" (dict "images" (list .Values.frontend.image) "context" $) -}}
{{- end -}}
{{/*
Return the proper Shuffle orborus image name
*/}}
{{- define "shuffle.orborus.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.orborus.image "global" .Values.global ) -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the orborus pod
*/}}
{{- define "shuffle.orborus.imagePullSecrets" -}}
{{- include "common.images.renderPullSecrets" (dict "images" (list .Values.orborus.image) "context" $) -}}
{{- end -}}
{{/*
Return the proper Shuffle worker image name
*/}}
{{- define "shuffle.worker.image" -}}
{{- include "common.images.image" ( dict "imageRoot" .Values.worker.image "global" .Values.global ) -}}
{{- end -}}
{{/*
Create the name of the service account to use for the Shuffle backend
*/}}
{{- define "shuffle.backend.serviceAccount.name" -}}
{{- if .Values.backend.serviceAccount.create -}}
{{ default (include "shuffle.backend.name" .) .Values.backend.serviceAccount.name | trunc 63 | trimSuffix "-" }}
{{- else -}}
{{ default "default" .Values.backend.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the backend service account
*/}}
{{- define "shuffle.backend.serviceAccount.imagePullSecrets" -}}
{{- $pullSecrets := list }}
{{- range .Values.global.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- range .Values.backend.serviceAccount.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) -}}
imagePullSecrets:
{{- range $pullSecrets | uniq }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
Create the name of the service account to use for the Shuffle frontend
*/}}
{{- define "shuffle.frontend.serviceAccount.name" -}}
{{- if .Values.frontend.serviceAccount.create -}}
{{ default (include "shuffle.frontend.name" .) .Values.frontend.serviceAccount.name | trunc 63 | trimSuffix "-" }}
{{- else -}}
{{ default "default" .Values.frontend.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the frontend service account
*/}}
{{- define "shuffle.frontend.serviceAccount.imagePullSecrets" -}}
{{- $pullSecrets := list }}
{{- range .Values.global.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- range .Values.frontend.serviceAccount.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) -}}
imagePullSecrets:
{{- range $pullSecrets | uniq }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
Create the name of the service account to use for Shuffle orborus
*/}}
{{- define "shuffle.orborus.serviceAccount.name" -}}
{{- if .Values.orborus.serviceAccount.create -}}
{{ default (include "shuffle.orborus.name" .) .Values.orborus.serviceAccount.name | trunc 63 | trimSuffix "-" }}
{{- else -}}
{{ default "default" .Values.orborus.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the orborus service account
*/}}
{{- define "shuffle.orborus.serviceAccount.imagePullSecrets" -}}
{{- $pullSecrets := list }}
{{- range .Values.global.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- range .Values.orborus.serviceAccount.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) -}}
imagePullSecrets:
{{- range $pullSecrets | uniq }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
Create the name of the service account to use for Shuffle workers
*/}}
{{- define "shuffle.worker.serviceAccount.name" -}}
{{- if .Values.worker.serviceAccount.create -}}
{{ default (include "shuffle.worker.name" .) .Values.worker.serviceAccount.name | trunc 63 | trimSuffix "-" }}
{{- else -}}
{{ default "default" .Values.worker.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the worker service account
*/}}
{{- define "shuffle.worker.serviceAccount.imagePullSecrets" -}}
{{- $pullSecrets := list }}
{{- range .Values.global.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- range .Values.worker.serviceAccount.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) -}}
imagePullSecrets:
{{- range $pullSecrets | uniq }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
Create the name of the service account to use for Shuffle apps
*/}}
{{- define "shuffle.app.serviceAccount.name" -}}
{{- if .Values.app.serviceAccount.create -}}
{{ default (include "shuffle.app.name" .) .Values.app.serviceAccount.name | trunc 63 | trimSuffix "-" }}
{{- else -}}
{{ default "default" .Values.app.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names for the app service account
*/}}
{{- define "shuffle.app.serviceAccount.imagePullSecrets" -}}
{{- $pullSecrets := list }}
{{- range .Values.global.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- range .Values.app.serviceAccount.imagePullSecrets -}}
{{- if kindIs "map" . -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" .name "context" .)) -}}
{{- else -}}
{{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" .)) -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) -}}
imagePullSecrets:
{{- range $pullSecrets | uniq }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}
@@ -0,0 +1,28 @@
# This PVC is always enabled, regardless of .Values.persistence.enabled,
# as app building does not work without it.
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: backend-apps-claim # Hardcoded by shuffle-app-builder
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
annotations:
{{- if eq .Values.persistence.resourcePolicy "keep" }}
helm.sh/resource-policy: keep
{{- end }}
{{- if or .Values.persistence.appBuilder.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.appBuilder.annotations .Values.commonAnnotations ) "context" . ) }}
{{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- range .Values.persistence.appBuilder.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.appBuilder.size }}
{{- if .Values.persistence.appBuilder.selector }}
selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.appBuilder.selector "context" $) | nindent 2 }}
{{- end }}
{{- include "common.storage.class" ( dict "persistence" .Values.persistence.appBuilder "global" .Values.global ) | nindent 2 }}
@@ -0,0 +1,30 @@
{{- if .Values.persistence.enabled }}
{{- if (not .Values.persistence.apps.existingClaim) }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ printf "%s-apps" (include "shuffle.backend.name" .) }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
annotations:
{{- if eq .Values.persistence.resourcePolicy "keep" }}
helm.sh/resource-policy: keep
{{- end }}
{{- if or .Values.persistence.apps.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.apps.annotations .Values.commonAnnotations ) "context" . ) }}
{{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- range .Values.persistence.apps.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.apps.size }}
{{- if .Values.persistence.apps.selector }}
selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.apps.selector "context" $) | nindent 2 }}
{{- end }}
{{- include "common.storage.class" ( dict "persistence" .Values.persistence.apps "global" .Values.global ) | nindent 2 }}
{{- end }}
{{- end }}

Some files were not shown because too many files have changed in this diff Show More