Merge branch 'nightly' into dependabot/go_modules/functions/onprem/worker/github.com/go-git/go-git/v5-5.13.0

This commit is contained in:
Frikky
2025-02-28 01:26:37 +01:00
committed by GitHub
141 changed files with 47235 additions and 16948 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- main
- nightly
paths:
- "**"
- "!.github/**"
@@ -20,19 +20,19 @@ jobs:
include:
- app: frontend
path: frontend
version: 2.0.0
version: nightly
experimental: true
- app: backend
path: backend
version: 2.0.0
version: nightly
experimental: true
- app: orborus
path: functions/onprem/orborus
version: 2.0.0
version: nightly
experimental: true
- app: worker
path: functions/onprem/worker
version: 2.0.0
version: nightly
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
+2 -1
View File
@@ -5,6 +5,7 @@ on:
workflows: ["dockerbuild"]
types:
- completed
workflow_dispatch:
jobs:
build:
@@ -19,7 +20,7 @@ jobs:
uses: actions/checkout@v2
- 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
+5 -5
View File
@@ -15,20 +15,20 @@ RUN wget -O /app_sdk/app_base.py https://raw.githubusercontent.com/Shuffle/app_s
ADD ./app_gen /app_gen
RUN go get -v
RUN go mod tidy
RUN go clean -modcache
#RUN go mod tidy
#RUN go clean -modcache
# From November 2022, CGO is enabled due to packages
# that we use requiring it. This is a temporary fix
# and makes us HAVE to install libc compatibility packages farther down.
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o webapp .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webapp .
# Certificate build - gets required certs
FROM alpine:latest as certs
RUN apk --update add ca-certificates
RUN apk add --update ca-certificates
# Sets up the final image
FROM alpine:3.17.0
FROM alpine:3.21.2
# FIXME: Install cgo because CGO_ENABLED=1 during build
RUN apk add --no-cache libc6-compat
+4 -4
View File
@@ -289,7 +289,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin
// Handles pulling of the same image if applicable
// This fixes some issues with older versions of Docker which can't build
// on their own ( <17.05 )
pullOptions := types.ImagePullOptions{}
pullOptions := image.PullOptions{}
downloaded := false
for _, image := range tags {
// Is this ok? Not sure. Tags shouldn't be controlled here prolly.
@@ -555,7 +555,7 @@ func imageCheckBuilder(images []string) error {
return err
}
allImages, err := client.ImageList(ctx, types.ImageListOptions{
allImages, err := client.ImageList(ctx, image.ListOptions{
All: true,
})
@@ -631,7 +631,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
}
ctx := context.Background()
images, err := dockercli.ImageList(ctx, types.ImageListOptions{
images, err := dockercli.ImageList(ctx, image.ListOptions{
All: true,
})
@@ -665,7 +665,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
}
}
pullOptions := types.ImagePullOptions{}
pullOptions := image.PullOptions{}
if len(img.ID) == 0 {
_, err := dockercli.ImagePull(context.Background(), version.Name, pullOptions)
if err == nil {
+25 -24
View File
@@ -1,17 +1,15 @@
module shuffle
go 1.22.0
go 1.22.2
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
toolchain go1.22.2
require (
cloud.google.com/go/datastore v1.15.0
cloud.google.com/go/storage v1.40.0
github.com/basgys/goxml2json v1.1.0
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/docker/docker v26.1.5+incompatible
github.com/docker/docker v27.5.0+incompatible
github.com/frikky/kin-openapi v0.42.0
github.com/fsouza/go-dockerclient v1.11.0
github.com/ghodss/yaml v1.0.0
@@ -20,10 +18,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.6.94
golang.org/x/crypto v0.31.0
github.com/shuffle/shuffle-shared v0.8.3
golang.org/x/crypto v0.32.0
google.golang.org/api v0.176.1
google.golang.org/grpc v1.63.2
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
@@ -35,10 +33,11 @@ require (
cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/auth v0.3.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/scheduler v1.10.6 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // 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
github.com/ProtonMail/go-crypto v1.1.3 // indirect
@@ -53,14 +52,14 @@ require (
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/frikky/schemaless v0.0.13 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -87,13 +86,14 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opensearch-project/opensearch-go v1.1.0 // indirect
github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
@@ -110,26 +110,27 @@ require (
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.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+46 -2
View File
@@ -17,6 +17,8 @@ cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbf
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=
cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg=
cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8=
@@ -24,6 +26,8 @@ cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/scheduler v1.10.6 h1:5U8iXLoQ03qOB+ZXlAecU7fiE33+u3QiM9nh4cd0eTE=
cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
@@ -35,6 +39,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krn
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=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -108,8 +114,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
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 v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug=
@@ -150,6 +160,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -283,8 +295,12 @@ github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/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=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -302,6 +318,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
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=
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
@@ -332,8 +350,6 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdR
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.94 h1:IWMwwrKjQgmmqpp6/Cy9gOT55+pbGrRgzExzXyiBHVU=
github.com/shuffle/shuffle-shared v0.6.94/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus=
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=
@@ -373,12 +389,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk=
@@ -387,10 +409,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU=
go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=
go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8=
@@ -406,6 +432,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
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 +490,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
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=
@@ -469,6 +499,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
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=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -507,12 +539,16 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
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=
@@ -601,8 +637,12 @@ google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUE
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=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -614,6 +654,8 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -627,6 +669,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+75 -10
View File
@@ -1048,6 +1048,20 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
licensed := shuffle.IsLicensed(ctx, *org)
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
if err != nil {
log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
orgApps := workflowapps
activatedAppIds := []string{}
for _, app := range orgApps {
activatedAppIds = append(activatedAppIds, app.ID)
}
returnValue := shuffle.HandleInfo{
Success: true,
Username: userInfo.Username,
@@ -1069,6 +1083,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
Interests: orgInterests,
Priorities: orgPriorities,
Licensed: licensed,
ActiveApps: activatedAppIds,
}
returnData, err := json.Marshal(returnValue)
@@ -3064,8 +3079,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."}`))
@@ -3139,7 +3155,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 {
@@ -3175,7 +3202,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
err = shuffle.DeployAppToDatastore(ctx, api)
//func DeployAppToDatastore(ctx context.Context, workflowapp WorkflowApp, bucketName string) error {
if err != nil {
log.Printf("Failed adding app to db: %s", err)
log.Printf("[ERROR] Failed adding app to db: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed adding app to db: %s"}`, err)))
return
@@ -3184,7 +3211,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
// 2. Get all the required code
appbase, staticBaseline, err := shuffle.GetAppbase()
if err != nil {
log.Printf("Failed getting appbase: %s", err)
log.Printf("[ERROR] Failed getting appbase: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed getting appbase code"}`))
return
@@ -3252,9 +3279,9 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
err = shuffle.SetUser(ctx, &user, true)
if err != nil {
log.Printf("[ERROR] Failed adding verification for user %s: %s", user.Username, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Failed updating user"}`)))
return
//resp.WriteHeader(500)
//resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Failed updating user"}`)))
//return
}
}
@@ -3264,6 +3291,10 @@ 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)
}
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.
@@ -4842,7 +4873,7 @@ func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) {
// FIXME - add org check too, and not just owner
// Check workflow.Sharing == private / public / org too
if user.Id != workflow.Owner || len(user.Id) == 0 {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[AUDIT] User %s is accessing workflow %s as admin (public)", user.Username, workflow.ID)
} else {
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (public)", user.Username, workflow.ID)
@@ -4939,6 +4970,10 @@ func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) {
return
}
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "The upload API is not yet implemented in self-hosted Shuffle. Please explore the app hotloading system: https://shuffler.io/docs/app_creation#uploading-an-app"}`))
return
//https://stackoverflow.com/questions/22964950/http-request-formfile-handle-zip-files
request.ParseMultipartForm(32 << 20)
f, _, err := request.FormFile("shuffle_file")
@@ -5088,6 +5123,8 @@ func initHandlers() {
r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}/activate", activateWorkflowAppDocker).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}/deactivate", activateWorkflowAppDocker).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}/distribute", activateWorkflowAppDocker).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/frameworkConfiguration", shuffle.GetFrameworkConfiguration).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/frameworkConfiguration", shuffle.SetFrameworkConfiguration).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}", shuffle.UpdateWorkflowAppConfig).Methods("PATCH", "OPTIONS")
@@ -5107,6 +5144,13 @@ func initHandlers() {
r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/authentication/group", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/authentication/group", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/authentication/group/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/authentication/groups", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/authentication/groups", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/authentication/groups/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS")
// Related to use-cases that are not directly workflows.
r.HandleFunc("/api/v1/workflows/usecases/{key}", shuffle.HandleGetUsecase).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS")
@@ -5157,6 +5201,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")
@@ -5173,9 +5227,11 @@ func initHandlers() {
r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/triggers/pipeline", shuffle.HandleNewPipelineRegister).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/triggers/github/register", shuffle.HandleNewGithubRegister).Methods("PUT", "OPTIONS")
//r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS")
r.HandleFunc("/api/v1/pipelines/{key}", handlePipelineCallback).Methods("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS")
r.HandleFunc("/api/v1/triggers", shuffle.HandleGetTriggers).Methods("GET", "OPTIONS")
//r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS")
@@ -5204,8 +5260,13 @@ func initHandlers() {
// This is a new API that validates if a key has been seen before.
// Not sure what the best course of action is for it.
r.HandleFunc("/api/v1/getenvironments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/setenvironments", shuffle.HandleSetEnvironments).Methods("PUT", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/rerun", shuffle.HandleRerunExecutions).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/stats", shuffle.HandleGetenvStats).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/config", shuffle.HandleSetenvConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/environments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/validate_app_values", shuffle.HandleKeyValueCheck).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/list_cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
@@ -5214,8 +5275,10 @@ func initHandlers() {
r.HandleFunc("/api/v1/orgs/{orgId}/set_cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/delete_cache", shuffle.HandleDeleteCacheKeyPost).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/cache/config", shuffle.HandleCacheConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleAppendStatistics).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
@@ -5226,7 +5289,7 @@ func initHandlers() {
r.HandleFunc("/api/v1/orgs/{orgId}/datastore/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
// Docker orborus specific - downloads an image
r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "GET", "OPTIONS")
r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "POST", "OPTIONS")
@@ -5242,6 +5305,8 @@ func initHandlers() {
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleGetFileMeta).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/files/{fileId}/config", shuffle.HandleSetFileConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/files/namespaces/{namespace}/share", shuffle.HandleShareNamespace).Methods("POST", "OPTIONS")
// This structure is horrendous. Needs fixing after we got the prototype up
r.HandleFunc("/api/v1/detections/{detectionType}/connect", shuffle.HandleDetectionAutoConnect).Methods("GET", "OPTIONS")
+8 -30
View File
@@ -20,8 +20,8 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/api/types/image"
//gyaml "github.com/ghodss/yaml"
@@ -614,7 +614,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
return
}
if len(workflowExecution.ExecutionOrg) > 0 && user.ActiveOrg.Id == workflowExecution.ExecutionOrg && user.Role == "admin" {
if len(workflowExecution.ExecutionOrg) > 0 && user.ActiveOrg.Id == workflowExecution.ExecutionOrg {
//log.Printf("[DEBUG] User %s is in correct org. Allowing org continuation for execution!", user.Username)
} else {
log.Printf("[WARNING] Bad authorization key when getting stream results %s.", actionResult.ExecutionId)
@@ -965,7 +965,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
}
if user.Id != workflow.Owner || len(user.Id) == 0 {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[INFO] User %s is deleting workflow %s as admin. Owner: %s", user.Username, workflow.ID, workflow.Owner)
} else {
log.Printf("[WARNING] Wrong user (%s) for workflow %s (delete workflow)", user.Username, workflow.ID)
@@ -1743,15 +1743,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
Environments: execInfo.Environments,
}
//executionRequestWrapper, err := getWorkflowQueue(ctx, environment)
//if err != nil {
// executionRequestWrapper = ExecutionRequestWrapper{
// Data: []ExecutionRequest{executionRequest},
// }
//} else {
// executionRequestWrapper.Data = append(executionRequestWrapper.Data, executionRequest)
//}
//log.Printf("Execution request: %#v", executionRequest)
executionRequest.Priority = workflowExecution.Priority
err = shuffle.SetWorkflowQueue(ctx, executionRequest, environment)
@@ -1939,7 +1930,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
if !executionAuthValid {
if user.Id != workflow.Owner && user.Role != "scheduler" && user.Role != fmt.Sprintf("workflow_%s", fileId) {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[AUDIT] Letting user %s execute %s because they're admin of the same org", user.Username, workflow.ID)
} else {
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID)
@@ -2029,7 +2020,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
}
if user.Id != workflow.Owner || len(user.Id) == 0 {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[AUDIT] User %s is accessing workflow %s as admin (stop schedule)", user.Username, workflow.ID)
} else {
log.Printf("[WARNING] Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID)
@@ -2039,19 +2030,6 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
}
}
//if user.Id != workflow.Owner || len(user.Id) == 0 {
// if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
// log.Printf("[INFO] User %s is accessing workflow %s as admin", user.Username, workflow.ID)
// } else if workflow.Public {
// log.Printf("[INFO] Letting user %s access workflow %s because it's public", user.Username, workflow.ID)
// } else {
// log.Printf("[WARNING] Wrong user (%s) for workflow %s (get workflow)", user.Username, workflow.ID)
// resp.WriteHeader(401)
// resp.Write([]byte(`{"success": false}`))
// return
// }
//}
schedule, err := shuffle.GetSchedule(ctx, scheduleId)
if err != nil {
log.Printf("[WARNING] Failed finding schedule %s", scheduleId)
@@ -2279,7 +2257,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
}
if user.Id != workflow.Owner || len(user.Id) == 0 {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[INFO] User %s is deleting workflow %s as admin. Owner: %s", user.Username, workflow.ID, workflow.Owner)
} else {
log.Printf("[WARNING] Wrong user (%s) for workflow %s (schedule start). Owner: %s", user.Username, workflow.ID, workflow.Owner)
@@ -4094,12 +4072,12 @@ func LoadSpecificApps(resp http.ResponseWriter, request *http.Request) {
appSdk := os.Getenv("SHUFFLE_APP_SDK_VERSION")
if len(appSdk) == 0 {
_, err := dockercli.ImagePull(ctx, "frikky/shuffle:app_sdk", types.ImagePullOptions{})
_, err := dockercli.ImagePull(ctx, "frikky/shuffle:app_sdk", image.PullOptions{})
if err != nil {
log.Printf("[WARNING] Failed to download new App SDK: %s", err)
}
} else {
_, err := dockercli.ImagePull(ctx, fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", "ghcr.io", "frikky", appSdk), types.ImagePullOptions{})
_, err := dockercli.ImagePull(ctx, fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", "ghcr.io", "frikky", appSdk), image.PullOptions{})
if err != nil {
log.Printf("[WARNING] Failed to download new App SDK %s: %s", err)
}
+1
View File
@@ -53,6 +53,7 @@ services:
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
- SHUFFLE_PASS_APP_PROXY=${SHUFFLE_PASS_APP_PROXY}
- SHUFFLE_STATS_DISABLED=true
- SHUFFLE_WORKER_SCALE=run
- SHUFFLE_LOGS_DISABLED=true
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest
env_file: .env
+6 -6
View File
@@ -1,5 +1,5 @@
# Build environment
FROM node:21 as builder
FROM node:23 as builder
ENV NODE_OPTIONS="--max-old-space-size=4096"
@@ -10,9 +10,10 @@ ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package.json /usr/src/app/package.json
# Nocache yarn install
#RUN yarn config set "strict-ssl" false -g
#RUN yarn install --network-timeout 1000000
RUN npm config set fetch-retries 3 # Number of retry attempts (default is 2)
RUN npm config set fetch-retry-mintimeout 5000 # Minimum wait time before retrying (in ms, default is 10000)
RUN npm config set fetch-retry-maxtimeout 60000 # Maximum wait time before retrying (in ms, default is 60000)
RUN npm config set fetch-timeout 60000 # Overall fetch timeout (in ms, default is 300000)
RUN npm install --timeout=60000 --legacy-peer-deps
# copy only required files to not trigger rebuilding every time
@@ -22,8 +23,7 @@ COPY ./src /usr/src/app/src/
COPY ./*.sh /usr/src/app/
COPY ./*.json /usr/src/app/
#RUN rm -rf /usr/src/app/node_modules/webpack
#RUN yarn build --verbose
RUN npm run build --loglevel verbose 2>&1
# Production environment
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+116 -63
View File
@@ -4,35 +4,36 @@ import { Link, Route, Routes, BrowserRouter, useNavigate } from "react-router-do
import { CookiesProvider } from "react-cookie";
import { removeCookies, useCookies } from "react-cookie";
import Workflows from "./views/Workflows";
import GettingStarted from "./views/GettingStarted";
import Workflows from "./views/Workflows.jsx";
import GettingStarted from "./views/GettingStarted.jsx";
import AngularWorkflow from "./views/AngularWorkflow.jsx";
import Header from "./components/NewHeader.jsx";
import HealthPage from "./components/HealthPage.jsx";
//import Header from "./components/Header.jsx";
import theme from "./theme";
import Apps from "./views/Apps";
import theme from "./theme.jsx";
import Apps from "./views/Apps.jsx";
import Apps2 from "./views/Apps2.jsx";
import AppCreator from "./views/AppCreator";
import AppCreator from "./views/AppCreator.jsx";
import DetectionDashBoard from "./views/DetectionDashboard.jsx";
import Welcome from "./views/Welcome.jsx";
import Dashboard from "./views/Dashboard.jsx";
import DashboardView from "./views/DashboardViews.jsx";
import AdminSetup from "./views/AdminSetup";
import Admin from "./views/Admin";
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 Introduction from "./views/Introduction";
import SetAuthentication from "./views/SetAuthentication";
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
import SetAuthentication from "./views/SetAuthentication.jsx";
import SetAuthenticationSSO from "./views/SetAuthenticationSSO.jsx";
import Search from "./views/Search.jsx";
import RunWorkflow from "./views/RunWorkflow.jsx";
import Admin2 from "./views/Admin2.jsx";
import LoginPage from "./views/LoginPage";
import SettingsPage from "./views/SettingsPage";
import LoginPage from "./views/LoginPage.jsx";
import SettingsPage from "./views/SettingsPage.jsx";
import KeepAlive from "./views/KeepAlive.jsx";
import { ThemeProvider } from "@mui/material/styles";
@@ -40,8 +41,8 @@ import CssBaseline from '@mui/material/CssBaseline';
import UpdateAuthentication from "./views/UpdateAuthentication.jsx";
import FrameworkWrapper from "./views/FrameworkWrapper.jsx";
import ScrollToTop from "./components/ScrollToTop";
import AlertTemplate from "./components/AlertTemplate";
import ScrollToTop from "./components/ScrollToTop.jsx";
import AlertTemplate from "./components/AlertTemplate.js";
import { isMobile } from "react-device-detect";
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
@@ -58,6 +59,7 @@ import Drift from "react-driftjs";
import { AppContext } from './context/ContextApi.jsx';
import Workflows2 from "./views/Workflows2.jsx";
import AppExplorer from "./views/AppExplorer.jsx";
// Production - backend proxy forwarding in nginx
var globalUrl = window.location.origin;
@@ -207,37 +209,27 @@ const App = (message, props) => {
}
{curpath.includes("/workflows") && curpath.includes("/run") ?
<div style={{ height: 60, }} />
:
isLoggedIn ?
<div style={{ position: 'fixed', top: 16, left: 10, zIndex: 100000 }}>
<LeftSideBar userdata={userdata} globalUrl={globalUrl} serverside={false} notifications={notifications} />
{ window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
<div style={{ minHeight: 68, maxHeight: 68 }}>
<Header
notifications={notifications}
setNotifications={setNotifications}
userdata={userdata}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
curpath={curpath}
{...props}
/>
</div>
:
<div style={{ minHeight: 68, maxHeight: 68, }}>
<Header
billingInfo={{}}
notifications={notifications}
setNotifications={setNotifications}
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
userdata={userdata}
curpath={curpath}
serverside={false}
isMobile={false}
{...props}
/>
</div>
}
) : (
<div style={{ position: 'fixed', top: 32, left: 10, zIndex: 100000 }}>
<LeftSideBar checkLogin={checkLogin} userdata={userdata} globalUrl={globalUrl} notifications={notifications} />
</div>
) }
{/*
<div style={{ height: 60 }} />
@@ -262,7 +254,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/admin"
path="/admin2"
element={
<Admin
userdata={userdata}
@@ -279,6 +271,24 @@ const App = (message, props) => {
/>
}
/>
<Route
exact
path="/admin"
element={
<Admin2
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
notifications={notifications}
setNotifications={setNotifications}
globalUrl={globalUrl}
checkLogin={checkLogin}
userdata={userdata}
{...props}
/>
}
/>
<Route exact path="/search" element={<Search serverside={false} isLoaded={isLoaded} userdata={userdata} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} {...props} /> } />
<Route
exact
@@ -387,7 +397,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/usecases"
path="/usecases2"
element={
<Dashboard
userdata={userdata}
@@ -400,7 +410,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/usecases2"
path="/usecases"
element={
<Usecases2
userdata={userdata}
@@ -426,7 +436,7 @@ const App = (message, props) => {
<Route exact path="/apps/authentication" element={<UpdateAuthentication serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} register={true} isLoaded={isLoaded} globalUrl={globalUrl} setCookie={setCookie} cookies={cookies} {...props} />} />
<Route
exact
path="/apps"
path="/apps2"
element={
<Apps
isLoaded={isLoaded}
@@ -439,7 +449,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/apps2"
path="/apps"
element={
<Apps2
serverside={false}
@@ -466,7 +476,8 @@ const App = (message, props) => {
/>
}
/>
<Route exact path="/apis/:appid" element={<ApiExplorerWrapper serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} isMobile={false} selectedApp={undefined} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
<Route exact path="/apps/:appid" element={<AppExplorer userdata={userdata} isLoggedIn={isLoggedIn} isLoaded={isLoaded} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
<Route exact path="/apis/:appid" element={<ApiExplorerWrapper serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} isMobile={false} selectedApp={undefined} isLoaded={isLoaded}globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
<Route
exact
path="/detections/sigma"
@@ -474,7 +485,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/workflows"
path="/workflows2"
element={
<Workflows
checkLogin={checkLogin}
@@ -491,7 +502,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/workflows2"
path="/workflows"
element={
<Workflows2
checkLogin={checkLogin}
@@ -554,6 +565,7 @@ const App = (message, props) => {
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
isLoggedIn={isLoggedIn}
{...props}
/>
}
@@ -567,6 +579,7 @@ const App = (message, props) => {
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
isLoggedIn={isLoggedIn}
{...props}
/>
}
@@ -635,18 +648,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={serverside}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboards"
element={
<DashboardViews
serverside={serverside}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboard/:key"
element={
<DashboardViews
serverside={serverside}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/dashboards/:key"
element={
<DashboardViews
serverside={serverside}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
wut={userdata}
/>
}
/>
<Route
exact
path="/welcome"
+227
View File
@@ -0,0 +1,227 @@
import React, { useState, useEffect, useContext, memo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import OrganizationTab from '../components/OrganizationTab.jsx';
import UserManagmentTab from '../components/UserManagmentTab.jsx';
import CacheView from "../components/CacheView.jsx";
import Files from "../components/Files.jsx";
import AppAuthTab from "../components/AppAuthTab.jsx";
import SchedulesTab from "../components/SchedulesTab.jsx";
import EnvironmentTab from "../components/EnvironmentTab.jsx";
import TenantsTab from "../components/TenantsTab.jsx";
import {
Business as BusinessIcon,
PermIdentity as PermIdentityIcon,
HttpsOutlined as HttpsOutlinedIcon,
InsertDriveFileOutlined as InsertDriveFileOutlinedIcon,
StorageOutlined as StorageOutlinedIcon,
AccessTimeOutlined as AccessTimeOutlinedIcon,
FmdGoodOutlined as FmdGoodOutlinedIcon,
GroupOutlined as GroupOutlinedIcon
} from '@mui/icons-material';
import theme from '../theme.jsx';
import { Button, Tooltip } from '@mui/material';
import { Index } from 'react-instantsearch-dom';
import { Context } from '../context/ContextApi.jsx';
const AdminNavBar = (props) => {
const location = useLocation();
const { globalUrl, userdata, isCloud, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props;
const [selectedItem, setSelectedItem] = useState("Organization");
const [isSelectedFiles, setIsSelectedFiles] = useState(true);
const [isSelectedDataStore, setIsSelectedDataStore] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams.get('tab');
if (tabName === "environments") {
setSelectedItem("Locations");
} else if (tabName === "suborgs") {
setSelectedItem("Tenants");
}else if (tabName === "cache") {
setSelectedItem("Datastore");
}else if (tabName) {
setSelectedItem(tabName.charAt(0).toUpperCase() + tabName.slice(1));
} else {
setSelectedItem("Organization");
}
}, [location.search]);
const items = [
{ iconSrc: <BusinessIcon />, alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } },
{ iconSrc: <PermIdentityIcon />, alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
{ iconSrc: <HttpsOutlinedIcon />, alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <StorageOutlinedIcon />, alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
{ iconSrc: <InsertDriveFileOutlinedIcon />, alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
{ iconSrc: <AccessTimeOutlinedIcon />, alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
{ iconSrc: <FmdGoodOutlinedIcon />, alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <GroupOutlinedIcon />, alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } }
];
const setConfig = (newValue) => {
setSelectedItem(newValue);
if (newValue === "App Auth") {
const tabName = newValue.toLowerCase().replace(/\s+/g, '_');
navigate(`?tab=${tabName}`, { replace: true });
} else {
const tabName = newValue.toLowerCase().replace(/\s+/g, '_');
navigate(`?tab=${tabName}`, { replace: true });
}
};
const renderComponent = () => {
const selectedItemData = items.find(item => item.text === selectedItem);
if (!selectedItemData) {
setSelectedItem("Organization");
// If no tab is specified, default to "Organization" tab
return <OrganizationTab globalUrl={globalUrl} removeCookie={removeCookie} selectedStatus={selectedStatus} isLoaded={isLoaded} setSelectedStatus={setSelectedStatus} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} handleGetOrg={handleGetOrg} userdata={userdata} isCloud={isCloud} serverside={serverside} notifications={notifications} checkLogin={checkLogin} setNotifications={setNotifications} stripeKey={stripeKey} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization}/>;
};
const ComponentToRender = selectedItemData.component;
const componentProps = selectedItemData.props;
return <ComponentToRender {...componentProps} />;
};
const defaultImage = "/images/logos/orange_logo.svg"
const imageData =
selectedOrganization?.image === undefined || selectedOrganization?.image.length === 0
? defaultImage
: selectedOrganization?.image;
return (
<Wrapper>
<div style={{ flexDirection: 'column', width: 220, }}>
<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={{
fontFamily: theme?.typography?.fontFamily,
fontSize: '16px',
color: "#FFFFFF",
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%',
marginLeft: 5,
}}>{selectedOrganization?.name}</div>
</div>
<div style={{ borderTop: '1px solid #494949', marginTop: 23 }} />
{items.map((item, index) => (
<Tooltip
key={index}
title={
((item.text === "Users") || (item.text === "Files") || (item.text === "Triggers") || (item.text === "Locations")) && !(userdata?.support || userdata?.active_org?.role === "admin")
? "Your role is not admin. Please ask the admin to change your role."
: ""
}
placement="right"
>
<span style={{ display: "inline-block", width: "100%" }}>
<Button
key={item.text}
variant="text"
color="primary"
sx={{
gap: 1,
"&:hover": {
backgroundColor: "#323232 !important",
},
"&.MuiButton-root": {
color: selectedItem === item.text ? "#FFFFFF" : "#9E9E9E",
fontSize: 16,
backgroundColor: "transparent",
textTransform: "none",
cursor: "pointer",
display: "flex",
alignItems: "center",
border: "none",
marginTop: index === 0 ? "15px" : "5px",
width: "100%",
justifyContent: "flex-start",
borderLeft:
selectedItem === item.text
? "3px solid rgba(255, 132, 68, 1)"
: "none",
borderTopLeftRadius: selectedItem === item.text ? "2.5px" : null,
borderBottomLeftRadius: selectedItem === item.text ? "2.5px" : null,
paddingLeft: selectedItem === item.text ? "15px" : "10px",
fontWeight: selectedItem === item.text ? 200 : "normal",
flex: 1,
},
"&.Mui-disabled": {
color: "#6F6F6F",
},
}}
disabled={
((item.text === "Users") || (item.text === "Files") || (item.text === "Triggers") || (item.text === "Locations")) && !(userdata?.support || userdata?.active_org?.role === "admin")
}
startIcon={item.iconSrc}
onClick={() => setConfig(item.text)}
>
{item.text.replace(/_/g, " ")}
</Button>
</span>
</Tooltip>
))}
</nav>
</div>
<Wrapper2>{renderComponent()}</Wrapper2>
</Wrapper>
);
};
export default AdminNavBar;
const PaddingWrapper2 = memo(({ children }) => {
return (
<div div style={{marginBottom: 30, width: "75%" , maxWidth: 1200, height: "100%", boxSizing: 'border-box'}}>
{children}
</div>
)
});
const Wrapper2 = memo(({children}) => {
return (
<PaddingWrapper2>
{children}
</PaddingWrapper2>
);
})
const PaddingWrapper = memo(({ children }) => {
const { leftSideBarOpenByClick, windowWidth } = useContext(Context);
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80,
transition: "padding-left 0.3s ease",
width: "100%",
overflow: "hidden",
height: "100%",
}}
>
{children}
</div>
);
});
const Wrapper = memo(({ children }) => {
return (
<PaddingWrapper>
{children}
</PaddingWrapper>
);
})
+275
View File
@@ -0,0 +1,275 @@
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");
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 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
};
}
// 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;
+63 -33
View File
@@ -25,7 +25,8 @@ import {
TableRow,
InputAdornment,
Divider,
LinearProgress
LinearProgress,
Tooltip,
} from "@mui/material";
import throttle from "lodash/throttle";
import theme from "../theme.jsx";
@@ -100,6 +101,8 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se
const [ExampleBody, setExampleBody] = useState({});
const [filteredActions, setFilteredActions] = useState([]);
const [firstSendDone, setFirstSendDone] = useState(false)
const getJsonObject = (properties) => {
let jsonObject = {};
@@ -1181,6 +1184,7 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se
}
}
}
var prefixCheck = "/v1";
if (parentUrl.includes("/")) {
const urlsplit = parentUrl.split("/");
@@ -1240,7 +1244,7 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se
}
}
newActions = newActions2;
newActions = newActions2
// Rearrange them by which has action_label
const firstActions = newActions.filter(
@@ -1255,9 +1259,9 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se
data.action_label === null ||
data.action_label === "No Label"
);
newActions = firstActions.concat(secondActions);
setActions(newActions);
setExampleBody(newActions[0]?.body);
newActions = firstActions.concat(secondActions)
setActions(newActions)
setExampleBody(newActions[0]?.body)
}, [openapi]);
return (
@@ -1425,36 +1429,47 @@ const ActionsList = memo(({
};
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
const query = e?.target?.value?.toLowerCase().replaceAll("_", " ");
setSearchQuery(query)
if (query.length === 0) {
setVisibleActions(actions);
setVisibleActions(actions)
} else {
setVisibleActions(
actions.filter((action) =>
action.name.toLowerCase().includes(searchQuery.toLowerCase())
actions?.filter((action) =>
action?.name?.toLowerCase()?.replaceAll("_", " ")?.includes(searchQuery)
)
);
)
}
};
}
return (
<div style={{ maxWidth: '350px', width: "25%", overflow: 'hidden', marginLeft: !(isLoggedIn || isLoaded) ? 5 : 0}}>
<div style={{ borderBottom: '1px solid #494949', paddingTop: 10, paddingBottom: 10}}>
<div>
{info?.title ? (
<div style={{ display: "flex", alignItems: "center" }}>
<img
src={openapi?.info["x-logo"]}
width={48}
height={48}
alt="app logo"
style={{ marginLeft: 20, borderRadius: 8 }}
/>
<Typography style={{ fontSize: 24, fontWeight: 'bold', marginLeft: 20, overflow: 'hidden', color: '#F1F1F1'
}}>
{info.title}
</Typography>
</div>
<a
href={`/apps/${openapi?.id}`}
style={{ textDecoration: 'none', }}
target="_blank"
rel="noreferrer"
>
<Tooltip title="Go to app" placement="right">
<div style={{ display: "flex", alignItems: "center" }}>
<img
src={openapi?.info["x-logo"]}
width={48}
height={48}
alt="app logo"
style={{ marginLeft: 20, borderRadius: 8 }}
/>
<Typography style={{ fontSize: 24, fontWeight: 'bold', marginLeft: 20, overflow: 'hidden', color: '#F1F1F1'
}}>
{info.title}
</Typography>
</div>
</Tooltip>
</a>
) : (
<Typography style={{ fontSize: 24, fontWeight: 'bold', marginLeft: 20, overflow: 'hidden', color: '#F1F1F1'
}}>
@@ -1463,9 +1478,10 @@ const ActionsList = memo(({
)}
</div>
</div>
<TextField
value={searchQuery}
placeholder="Search here"
placeholder="Search endpoints"
onChange={handleSearch}
InputProps={{
startAdornment: (
@@ -1483,6 +1499,7 @@ const ActionsList = memo(({
},
}}
/>
<div
style={{
marginLeft: 20,
@@ -1542,7 +1559,7 @@ const ActionsList = memo(({
overflow: "hidden",
}}
>
{action.name.replaceAll("_", " ")}
{action?.name?.replaceAll("_", " ")}
</span>
</Button>
))
@@ -1794,7 +1811,7 @@ const Action = memo((
response.result = JSON.parse(response.result);
} catch (parseError) {
console.error("Error parsing result:", parseError);
toast.error("Error parsing response result.");
//toast.error("Error parsing response result.");
}
}
@@ -2035,6 +2052,7 @@ const Action = memo((
}, [editorRef.current]);
const actionname = action?.name.charAt(0).toUpperCase() + action?.name.slice(1).replaceAll("_", " ")
return (
<div
ref={actionRef}
@@ -2042,6 +2060,7 @@ const Action = memo((
data-index={index}
key={action.name.replace(/ /g, "-").replace(/_/g, "-")}
style={{
marginTop: 50,
display: "flex",
flexDirection: "row",
minHeight: 400,
@@ -2066,7 +2085,7 @@ const Action = memo((
color: "rgba(241, 241, 241, 1)",
}}
>
{action.name}
{actionname}
</Typography>
<div
style={{
@@ -2222,11 +2241,12 @@ const Action = memo((
}}
onChange={(e) => {
const newUrl = e.target.value;
const newUrl = e.target.value
setActionUrl(newUrl);
const params = extractParamsFromText(newUrl);
setRequestParams(params);
if (newUrl.startsWith("http://") || newUrl.startsWith("https://")) {
try {
const url = new URL(newUrl);
@@ -2235,9 +2255,12 @@ const Action = memo((
setPath(newPath);
} catch (error) {
console.error("Invalid URL:", error);
console.error("Invalid URL:", newUrl, error);
toast("The URL is not a valid one. Please check and try again.")
}
}
} else {
//toast("The URL needs to start with http:// or https://")
}
}}
/>
@@ -2259,6 +2282,13 @@ const Action = memo((
}}
disabled={disableExecuteButton}
onClick={() => {
/*
if (!firstSendDone) {
setFirstSendDone(true)
setCurTab(2)
}
*/
if (actionUrl.length === 0) {
toast.error("URL cannot be empty");
return;
File diff suppressed because it is too large Load Diff
+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
+28 -24
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,11 +414,15 @@ const AppGrid = (props) => {
scrollbarColor: "#494949 #2f2f2f",
}}
>
{hits.map((data, index) => {
{hits?.map((data, index) => {
const appUrl =
isCloud
? `/apps/${data.objectID}?queryID=${data.__queryID}`
: `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`;
isCloud === true ?
`/apps/${data.objectID}`
: `apps/${data.objectID}`;
if (data.name === "" && data.id === "") {
return null
}
return (
<Zoom
@@ -552,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 ? ", " : ""}
@@ -565,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 ? ", " : ""}
@@ -756,7 +760,7 @@ const AppGrid = (props) => {
};
const transformRefinementListItems = items =>
items.map(item => ({
items?.map(item => ({
...item,
label: item.label === 'true' ? 'App Editor' : 'Python',
}));
@@ -1099,7 +1103,7 @@ const AppGrid = (props) => {
}
});
const categoryArray = Object.keys(categoryCountMap).map((category) => ({
const categoryArray = Object.keys(categoryCountMap)?.map((category) => ({
category,
count: categoryCountMap[category],
}));
@@ -1165,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={{
@@ -1243,7 +1247,7 @@ const AppGrid = (props) => {
});
}
const tagArray = Object.keys(tagCountMap).map((tag) => ({
const tagArray = Object.keys(tagCountMap)?.map((tag) => ({
tag,
count: tagCountMap[tag],
}));
@@ -1304,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)}
@@ -1415,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",
@@ -1685,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;
@@ -1732,13 +1736,13 @@ const AppGrid = (props) => {
};
const appUrl =
isCloud === true
? `/apps/${data.id}`
isCloud === true ?
`/apps/${data.id}`
: `https://shuffler.io/apps/${data.id}`;
if (data.name === "" && data.id === "") {
return null
}
if (data.name === "" && data.id === "") {
return null
}
return (
<Zoom
@@ -1843,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 ? ", " : ""}
@@ -1884,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.")
+163 -21
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import {
@@ -23,14 +23,14 @@ import ForkRightIcon from '@mui/icons-material/ForkRight';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import LaunchIcon from '@mui/icons-material/Launch';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { CloudDownloadOutlined } from '@mui/icons-material';
import { CloudDownloadOutlined, Delete } from '@mui/icons-material';
import { findSpecificApp } from '../components/AppFramework.jsx';
import theme from "../theme.jsx";
import YAML from 'yaml';
import { toast } from 'react-toastify';
import { Link } from 'react-router-dom';
const AppModal = ({ open, onClose, app, globalUrl }) => {
const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
const [frameworkData, setFrameworkData] = useState({})
const [userdata, setUserdata] = useState({})
@@ -41,6 +41,8 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
const [latestUsecase, setLatestUsecase] = useState([])
const [foundAppUsecase, setFoundAppUsecase] = useState({})
const [usecaseLoading, setUsecaseLoading] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [sharingConfiguration, setSharingConfiguration] = React.useState("you");
const navigate = useNavigate();
const parseUsecase = (subcase) => {
const srcdata = findSpecificApp(frameworkData, subcase.type)
@@ -247,6 +249,11 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
}
const getUsecase = (subcase, index, subindex) => {
//console.log("Skipping getUsecase")
// FIXME: Skipping for now as this screws over a lot of the prioritization system
// due to them having "looked at" the usecase.
return
subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
@@ -294,8 +301,101 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
setUsecaseLoading(true)
getAvailableWorkflows()
getFramework()
handleUpdateSharingConfiguration()
}, [app])
const handleUpdateSharingConfiguration = useCallback(() => {
if (app?.sharing === true) {
setSharingConfiguration("public")
}else {
setSharingConfiguration("you")
}
}, [app?.id])
const deleteApp = (appId) => {
toast("Attempting to delete app");
fetch(globalUrl + "/api/v1/apps/" + appId, {
method: "DELETE",
headers: {
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status === 200) {
toast("Successfully deleted app");
setTimeout(() => {
//delete apps from local storage
localStorage.removeItem("apps");
getApps();
}, 1000);
} else {
toast("Failed deleting app. Does it still exist?");
}
})
.catch((error) => {
toast(error.toString());
});
};
const deleteModal = deleteModalOpen ? (
<Dialog
open={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
}}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "500px",
overflow: "hidden",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
>
<DialogTitle>
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.9)" }}>
Are you sure? <div />
Some workflows may stop working.
</div>
</DialogTitle>
<DialogContent
style={{ color: "rgba(255,255,255,0.65)", textAlign: "center" }}
>
<Button
style={{}}
onClick={() => {
deleteApp(app.id);
setDeleteModalOpen(false);
}}
color="primary"
>
Yes
</Button>
<Button
variant="outlined"
style={{marginLeft: 5}}
onClick={() => {
setDeleteModalOpen(false);
}}
color="primary"
>
No
</Button>
</DialogContent>
</Dialog>
) : null;
const downloadApp = (inputdata) => {
const id = inputdata.id;
@@ -382,7 +482,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io" || window.location.host === "localhost:3000"
window.location.host === "shuffler.io"
? true
: false;
@@ -395,8 +495,10 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
}
var canEditApp = userdata !== undefined && (userdata?.admin === "true" || userdata?.id === app?.owner || app?.owner === "" || (userdata?.admin === "true" && userdata?.active_org?.id === app?.reference_org)) || !app?.generated
var canEditApp = userdata?.support ||
userdata?.id === app?.owner ||
(userdata?.admin === "true" && userdata?.active_org?.id === app?.reference_org) ||
app?.contributors?.includes(userdata?.id)
return (
<Dialog
open={open}
@@ -425,6 +527,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
}
}}
>
{deleteModal}
<DialogTitle
sx={{
display: 'flex',
@@ -437,15 +540,22 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
fontFamily: theme?.typography?.fontFamily
}}
>
{/*
<Typography component="div" sx={{ fontWeight: 500, color: "#F1F1F1", fontSize: "20px" }}>
About {app?.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
</Typography>
*/}
<IconButton
onClick={onClose}
sx={{
color: 'rgba(255, 255, 255, 0.7)',
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
style={{
position: "absolute",
top: 10,
right: 10,
}}
>
<CloseIcon />
</IconButton>
@@ -457,7 +567,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
<div style={{ display: "flex", flexDirection: "row", gap: 10, fontFamily: theme?.typography?.fontFamily }}>
<img
alt={app?.name}
src={app?.large_image || app?.image_url}
src={app?.large_image || app?.image_url || "/images/no_image.png"}
style={{
borderRadius: 4,
maxWidth: 100,
@@ -474,12 +584,16 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
flexDirection: "row",
alignItems: "center",
}}>
<Typography variant="h5" component="div" sx={{ fontWeight: 600 }}>
{newAppname}
</Typography>
<a href={isCloud ? "/apps/" + (app?.id || app?.objectID) : `https://shuffler.io/apps/${app?.objectID || app?.id}`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "rgba(255,255,255,0.9)", }}>
<Typography variant="h5" component="div" sx={{ fontWeight: 600 }}>
{newAppname}
</Typography>
</a>
<Link
to={"/apps/" + (app?.id || app?.objectID)}
to={isCloud ? `/apps/${app?.id || app?.objectID}` : `/apps/${app?.id || app?.published_id || app?.objectID}`}
style={{ textDecoration: "none", color: "#f85a3e", marginTop: "-2px" }}
target="_blank"
rel="noopener noreferrer"
>
<IconButton
style={{
@@ -536,6 +650,36 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
</Button>
</Tooltip>
) : null}
{(userdata?.id === app?.owner)? (
<Tooltip title={"Delete app (confirm box will show)"}>
<Button
variant="outlined"
component="label"
color="primary"
sx={{
bgcolor: '#494949',
'&:hover': { bgcolor: '#494949', border: 'none' },
textTransform: 'none',
borderRadius: 1,
minWidth: '45px',
width: '45px',
height: '40px',
padding: 2,
color: "#fff",
fontFamily: theme?.typography?.fontFamily,
border: 'none'
}}
onClick={() => {
setDeleteModalOpen(true);
}}
disabled={(sharingConfiguration === undefined || sharingConfiguration === null || sharingConfiguration === "public") }
>
<Delete />
</Button>
</Tooltip>
): null}
{(canEditApp && app?.generated) && (
<Button
variant="contained"
sx={{
@@ -549,10 +693,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
color: "#fff",
fontFamily: theme?.typography?.fontFamily
}}
startIcon={canEditApp ? <EditIcon /> :
(app?.generated && app?.activated && userdata?.id !== app?.owner && isCloud ?
<ForkRightIcon /> : null
)}
startIcon={canEditApp ? <EditIcon /> : <ForkRightIcon />}
onClick={() => {
if (canEditApp) {
const editUrl = "/apps/edit/" + (app?.id || app?.objectID);
@@ -563,8 +704,9 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
}
}}
>
{canEditApp ? "Edit" : "Fork"}
</Button>
{canEditApp ? "Edit" : "Fork"}
</Button>
)}
</div>
</Box>
@@ -685,13 +827,13 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
marginBottom: "16px",
fontWeight: 600
}}>
{
{/*
(foundAppUsecase?.srcapp !== undefined && foundAppUsecase?.dstapp !== undefined) ? (
"Connect " + foundAppUsecase?.srcapp?.replaceAll("_", " ") + " to " + foundAppUsecase?.dstapp?.replaceAll("_", " ")
) : (
"Connect " + app?.name.replaceAll("_", " ") + " to any tool"
)
}
*/}
</div>
)}
@@ -703,7 +845,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
alignItems: 'center',
mb: 3
}}>
{usecaseLoading ? (
{/*usecaseLoading ? (
<Stack direction="row" spacing={2} alignItems="center" sx={{ width: '100%' }}>
<Stack direction="row" spacing={-1}>
<Skeleton variant="circular" width={32} height={32} />
@@ -752,7 +894,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
{foundAppUsecase?.name || "Search for a Usecase"}
</Typography>
</>
)}
)*/}
</Box>
</div>
+214
View File
@@ -0,0 +1,214 @@
import React, { useState, useEffect, useRef } from 'react';
import theme from '../theme.jsx';
import { Link, useNavigate } from 'react-router-dom';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
//import algoliasearch from 'algoliasearch/lite';
import algoliasearch from 'algoliasearch';
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import {
Grid,
Paper,
TextField,
InputAdornment,
Typography,
} from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder,
} = props
let navigate = useNavigate();
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
const [open, setOpen] = React.useState(false);
const [value, setValue] = useState("");
window.title = "Shuffle | Apps | Find and integration any app"
const useCloseOnBlur = (setOpen) => {
useEffect(() => {
// Add event listener to detect clicks outside of the search box
const handleClickOutside = (event) => {
// Check if the click is outside the search box
if (!event.target.closest('.search-box')) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
// Clean up event listener
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [setOpen]); // Ensure that this effect runs whenever setOpen changes
};
useCloseOnBlur(setOpen);
const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => {
useEffect(() => {
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
if (defaultSearch !== undefined && defaultSearch !== null) {
refine(defaultSearch)
}
}, [])
return (
<div style={{ textAlign: isMobile ? "center" : "" }}>
<form noValidate action="" role="search">
<TextField
sx={{
'& input[type="search"]': {
filter:
'brightness(0) invert(1)',
},
}}
fullWidth
variant="standard"
style={{
width: isMobile ? 295 : 402,
borderRadius: 8,
border: 0,
boxShadow: 0,
margin: 10,
height: isMobile ? 51 : "",
textAlign: "center",
// border: "1px solid rgba(241.19, 241.19, 241.19, 0.10)",
boxShadow: "none",
backgroundColor: "rgba(241.19, 241.19, 241.19, 0.10)",
fontWeight: 400,
marginLeft: 10,
zIndex: 110,
}}
InputProps={{
disableUnderline: true,
style: {
fontSize: isMobile ? "0.8em" : "1em",
height: 50,
zIndex: 1100,
paddingLeft: 15,
},
endAdornment: (
<InputAdornment position="end">
<div
style={{
width: isMobile ? 42 : 42,
height: isMobile ? 36 : 36,
background: "#806BFF",
borderRadius: 8,
marginRight: 10,
}}
>
<SearchIcon
onClick={() => {
navigate("/search?q=" + currentRefinement, { state: value, replace: true })
//navigate("/search?q="+currentRefinement, { state: value, replace: true })
//window.open("/apps"+currentRefinement, "_blank")
}}
style={{ cursor: 'pointer', width: isMobile ? 20 : "", marginright: 5, marginTop: isMobile ? 6 : 7 }}
/>
</div>
</InputAdornment>
),
}}
autoComplete="off"
type="search"
color="primary"
placeholder={placeholder !== undefined ? placeholder : "Search more than 2500 Apps"}
id="shuffle_search_field"
onChange={(event) => {
// Remove "q" from URL
// removeQuery("q")
refine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if (event.keyCode === 13) {
navigate("/search?q=" + currentRefinement, { state: value, replace: true });
}
}}
onClick={(event) => {
setOpen(true);
}}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
</div>
)
}
const Hits = ({ hits, currentRefinement }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={0} style={{ display: "flex", alignItems: "center", justifyContent: "center", width: isMobile ? 302 : 402, maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => {
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,1)" : "#38383A",
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
// border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e",
textAlign: "left",
padding: 10,
cursor: "pointer",
position: "relative",
overflow: "hidden",
width: 402,
minHeight: 37,
maxHeight: 52,
}
if (counted === 12 / xs * rowHandler) {
return null
}
counted += 1
var parsedname = data.name.valueOf()
parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ")
return (
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}} onMouseOut={() => {
setMouseHoverIndex(-1)
}} onClick={() => {
if (setNewSelectedApp !== undefined) {
setNewSelectedApp(data.name)
} else {
//need to add perfect url which redirect direct to app page
const newname = data.name.toLowerCase().replaceAll(" ", "_")
window.open("/apps/" + newname, "_blank")
}
}}>
<div style={{ display: "flex" }}>
<img alt={data.name} src={data.image_url} style={{ width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, borderRadius: 40, maxHeight: 30, display: "block", }} />
<Typography variant="body1" style={{ marginTop: 2, marginLeft: 10, }}>
{parsedname}
</Typography>
</div>
</Paper>
)
})}
</Grid>
)
}
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(InputHits)
return (
<div className="search-box" style={{ width: isMobile ? null : "100%", height: 95, alignItems: "center", justifyContent: "center", gap: 138, zIndex: 11000, }}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
<div style={{ maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
<div style={{ alignItems: "center", justifyContent: "center", width: "100%", display: "flex" }}>
{open ? <CustomHits hitsPerPage={1} /> : null}
</div>
</InstantSearch>
</div>
)
}
export default Appsearch;
+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}
>
+368
View File
@@ -0,0 +1,368 @@
import React, { useState, useEffect } from 'react';
import classNames from "classnames";
import theme from '../theme.jsx';
import {
Tooltip,
TextField,
IconButton,
Button,
Typography,
Grid,
Paper,
Chip,
Checkbox,
} from "@mui/material";
import {
BarChart,
RadialBarChart,
RadialAreaChart,
RadialAxis,
StackedBarSeries,
TooltipArea,
ChartTooltip,
TooltipTemplate,
RadialAreaSeries,
RadialPointSeries,
RadialArea,
RadialLine,
TreeMap,
TreeMapSeries,
TreeMapLabel,
TreeMapRect,
Line,
LineChart,
LineSeries,
LinearYAxis,
LinearXAxis,
LinearYAxisTickSeries,
LinearXAxisTickSeries,
Area,
AreaChart,
AreaSeries,
AreaSparklineChart,
PointSeries,
GridlineSeries,
Gridline,
Stripes,
Gradient,
GradientStop,
LinearXAxisTickLabel,
} from 'reaviz';
const inputdata = {
"data": [{
"key": "Intel",
"data": [
{ key: new Date('11/22/2019'), data: 3, metadata: {color: "green", "name": "Intel"}},
{ key: new Date('11/24/2019'), data: 8, metadata: {color: "green", "name": "Intel"}},
{ key: new Date('11/29/2019'), data: 2, metadata: {color: "green", "name": "Intel"}},
]
},
{
"key": "Popper",
"data": [
{ key: new Date('11/24/2019'), data: 9, metadata: {color: "red", "name": "Popper"}},
{ key: new Date('11/29/2019'), data: 3, metadata: {color: "red", "name": "Popper"}},
]
}]
}
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
//console.log("Date: ", new Date("2019-11-14T08:00:00.000Z"))
//var inputdata = keys.data
//const inputdata = keys.data === undefined ? [{"key": inputname, "data": keys}] : keys.data
const inputdata = keys.data === undefined ? keys : keys.data
/*
series={
<AreaSeries
symbols={
<PointSeries show={false} />
}
area={
<Area
mask={<Stripes />}
gradient={
<Gradient
stops={[
<GradientStop offset="10%" stopOpacity={0} />,
<GradientStop offset="80%" stopOpacity={1} />
]}
/>
}
/>
}
gridlines={<GridlineSeries line={<Gridline direction="x" />} />}
colorScheme={(colorInput) => {
var color = "#f86a3e"
//if (colorInput !== undefined && colorInput.length > 0) {
// color = colorInput[0].metadata !== undefined && colorInput[0].metadata.color !== undefined ? colorInput[0].metadata.color : color
//}
return color
}}
/>
}
*/
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: 30, marginTop: 15, }}>
<Typography variant="h4" style={{marginBotton: 15, }}>
{inputname}
</Typography>
<BarChart
width={"100%"}
height={height}
data={inputdata}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
{/*
<AreaSparklineChart
style={{marginTop: 15, color: "white",}}
height={height}
width={width}
data={inputdata}
tooltip={
<Tooltip
tooltip={
<ChartTooltip
color={"#ffffff"}
followCursor={true}
modifiers={{
offset: '5px, 5px'
}}
content={(data, color) => (
<TooltipTemplate
color={"#ffffff"}
value={{
x: data.x,
y: data.y,
}}
/>
)}
/>
}
/>
}
/>
*/}
</div>
)
}
const AppStats = (defaultprops) => {
const { globalUrl, appId , workflowId} = defaultprops;
const [keys, setKeys] = useState([])
const [widgetData, setWidgetData] = useState({});
const [searches, setSearches] = useState([]);
const [clickData, setClickData] = useState(undefined);
const [conversionData, setConversionData] = useState(undefined);
const handleDataSetting = (inputdata, grouping) => {
var newlist = []
for (var key in inputdata.events) {
var newlist = []
for (var subkey in inputdata.events[key].data) {
const subdata = inputdata.events[key].data[subkey]
//console.log("Timestamp: ", subdata.key)
if (grouping === "day") {
const daysplit = subdata.key.split("T")[0]
//console.log("Grouping by day: ", daysplit)
const foundIndex = newlist.findIndex(data => data.key === daysplit)
if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
newlist[foundIndex].data += 1
newlist[foundIndex].y += 1
} else {
newlist.push({
"key": daysplit,
"x": daysplit,
"data": 1,
"y": 1,
})
}
} else {
console.log("No grouping set?")
try {
inputdata.events[key].data[subkey].key = new Date(subdata.key)
} catch (e) {
console.log("Failed timestamp: ", e)
}
}
}
// Fixing timestamps after sorting based on day
for (var subkey in newlist) {
const subdata = newlist[subkey]
newlist[subkey].key = new Date(subdata.key)
}
console.log("Inputdata: ", inputdata.events[key])
if (inputdata.events[key].key === "click") {
setClickData(newlist)
} else if (inputdata.events[key].key === "conversion") {
setConversionData(newlist)
} else {
console.log("No handler for ", inputdata.events[key].key)
}
}
//new Date('11/22/2019')
setWidgetData(inputdata)
}
const getAppStats = (appId) => {
fetch(`${globalUrl}/api/v1/apps/${appId}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
const getWorkflowStats = (workflowId) => {
fetch(`${globalUrl}/api/v1/workflow/${workflowId}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
//setWidgetData(inputdata)
getAppStats(appId)
getWorkflowStats(workflowId)
}, [])
const paperStyle = {
textAlign: "center",
padding: 40,
margin: 5,
backgroundColor: theme.palette.inputColor,
}
console.log("Widget: ", widgetData)
const data = (
<div className="content" style={{width: "100%", margin: "auto", paddingBottom: 200, textAlign: "center",}}>
<div style={{display: "flex", margin: "auto", }}>
<Paper style={paperStyle}>
<Typography variant="h4">
{widgetData.orgs}
</Typography>
<Typography variant="h6">
Orgs
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{widgetData.searches}
</Typography>
<Typography variant="h6">
Searches
</Typography>
</Paper>
{/*
<Paper style={paperStyle}>
<Typography variant="h4">
{widgetData.clicks}
</Typography>
<Typography variant="h6">
Clicks
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{widgetData.conversions}
</Typography>
<Typography variant="h6">
Conversions
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{widgetData.forks}
</Typography>
<Typography variant="h6">
Forks
</Typography>
</Paper>
*/}
</div>
{clickData === undefined || clickData === null || clickData?.length === 0 ?
null
:
<LineChartWrapper keys={clickData} height={300} width={"100%"} inputname={"Clicks"}/>
}
<div style={{marginTop: 25, }} />
{conversionData === undefined || conversionData === null || conversionData?.length === 0 ?
null
:
<LineChartWrapper keys={conversionData} height={300} width={"100%"} inputname={"Conversions"}/>
}
</div>
)
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
);
return dataWrapper;
}
export default AppStats;
+3
View File
@@ -50,6 +50,9 @@ const Appsearch = props => {
return (
<form noValidate action="" role="search">
<TextField
autoFocus
autoComplete="off"
autocomplete="off"
fullWidth
style={{backgroundColor: "#2F2F2F", borderRadius: borderRadius, width: "100%",}}
InputProps={{
+5 -3
View File
@@ -215,8 +215,8 @@ const Billing = memo((props) => {
var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1"
const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billing&payment=failure`
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
var checkoutObject = {
lineItems: [
{
@@ -2361,7 +2361,8 @@ const Billing = memo((props) => {
</div>
</div>
)}
<div style={{ marginTop: 40, marginLeft: 10 }}>
{isCloud ? (
<div style={{ marginTop: 40, marginLeft: 10 }}>
<Typography
style={{ marginBottom: 5, fontSize: 24, fontWeight: "bold" }}
>
@@ -2545,6 +2546,7 @@ const Billing = memo((props) => {
</Button>
</div>
): null}
</div>
<div style={{ marginTop: 40, display: 'flex', flexDirection: 'column' }}>
<Typography
+528 -38
View File
@@ -20,11 +20,18 @@ import {
DialogTitle,
DialogActions,
Skeleton,
Chip,
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,
@@ -46,6 +53,8 @@ import {
Business as BusinessIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
Clear as ClearIcon,
Add as AddIcon,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
@@ -68,7 +77,7 @@ const scrollStyle2 = {
const CacheView = memo((props) => {
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props;
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props;
const [orgCache, setOrgCache] = React.useState("");
const [listCache, setListCache] = React.useState([]);
const [addCache, setAddCache] = React.useState("");
@@ -82,14 +91,43 @@ const CacheView = memo((props) => {
const [editCache, setEditCache] = React.useState(false);
const [cachedLoaded, setCachedLoaded] = React.useState(false);
const [show, setShow] = useState({});
useEffect(() => {
if(orgId?.length >0){
listOrgCache(orgId);
}
}, [orgId]);
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
const [selectedCacheKey, setSelectedCacheKey] = useState("");
const listOrgCache = (orgId) => {
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
// 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, selectedCategory)
}
}, [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, category) => {
const url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache${category !== undefined ? `?category=${category.replaceAll(" ", "_")}` : ""}`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -109,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 !== "") {
@@ -122,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, {
@@ -145,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?");
@@ -157,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`, {
@@ -181,7 +234,7 @@ const CacheView = memo((props) => {
.then((responseJson) => {
setAddCache(responseJson);
toast("Cache Edited Successfully!");
listOrgCache(orgId);
listOrgCache(orgId, selectedCategory);
setModalOpen(false);
})
.catch((error) => {
@@ -190,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`, {
@@ -214,8 +271,8 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New Cache Added Successfully!");
listOrgCache(orgId);
toast("New key added Successfully!");
listOrgCache(orgId, selectedCategory);
setModalOpen(false);
})
.catch((error) => {
@@ -233,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,
})
}
}
@@ -296,13 +355,14 @@ const CacheView = memo((props) => {
>
<DialogTitle>
<span style={{ color: "white" }}>
{ editCache ? "Edit Cache" : "Add Cache" }
{ 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={{
@@ -335,7 +395,7 @@ const CacheView = memo((props) => {
autoFixJson(value)
}}
>
<AutoFixHighIcon />
<AutoFixNormalIcon />
</IconButton>
</Tooltip>
</div>
@@ -368,6 +428,7 @@ const CacheView = memo((props) => {
style={{ borderRadius: "2px", fontSize: 16, color: "#ff8544", textTransform:"none" }}
onClick={() => {
setModalOpen(false)
setKey("")
setValue("")
setDataValue({})
}}
@@ -379,8 +440,12 @@ const CacheView = memo((props) => {
variant="contained"
style={{ borderRadius: "2px", backgroundColor: "#ff8544",color: "#1a1a1a", textTransform:"none" }}
onClick={() => {
if (value === "") {
toast("Key or Value can not be empty");
return;
}
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
setKey("")
setValue("")
setDataValue({})
}}
@@ -392,13 +457,182 @@ const CacheView = memo((props) => {
</Dialog>
);
const handleSelectSubOrg = (id, action) => {
if (action === "all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
setSelectedSubOrg((prev) => {
if (prev.length === childOrgs.length) {
// If all child orgs are already selected, clear the selection
return [];
} else {
// Otherwise, select all child org IDs
return childOrgs.map((data) => data.id);
}
});
} else if (action === "none") {
setSelectedSubOrg([]);
} else {
setSelectedSubOrg((prev) => {
if (prev.includes(id)) {
return prev.filter((data) => data !== id);
} else {
return [...prev, id];
}
});
}
};
const changeDistribution = (id, selectedSubOrg) => {
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
}
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());
});
};
const cacheDistributionModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => setShowDistributionPopup(false)}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<div style={{ color: "rgba(255,255,255,0.9)" }}>
Select sub-org to distribute Datastore key
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
return (
<MenuItem
key={index}
value={data.id}
onClick={() => handleSelectSubOrg(data.id)}
style={{ display: "flex", alignItems: "center" }}
>
<Checkbox
checked={selectedSubOrg.includes(data.id)}
/>
{image}
<span style={{ marginLeft: 8 }}>{data.name}</span>
</MenuItem>
);
})}
<div style={{ display: "flex", marginTop: 20 }}>
<Button
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: "#ff8544" }}
onClick={() => setShowDistributionPopup(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: "#1a1a1a", backgroundColor: "#ff8544", marginLeft: 10 }}
onClick={() => {
changeDistribution(selectedCacheKey, selectedSubOrg);
}}
color="primary"
>
Submit
</Button>
</div>
</DialogContent>
</Dialog>
) : null;
return (
<div style={{paddingBottom: isSelectedDataStore?null:250, minHeight: 1000, boxSizing: "border-box", width: isSelectedDataStore? "100%" :null, transition: "width 0.3s ease", padding:isSelectedDataStore?"27px 10px 27px 27px":null, height: isSelectedDataStore?"100%":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderTopRightRadius: isSelectedDataStore?'8px':null, borderBottomRightRadius: isSelectedDataStore?'8px':null, borderLeft: "1px solid #494949" }}>
{modalView}
{cacheDistributionModal}
<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
@@ -422,16 +656,181 @@ const CacheView = memo((props) => {
setValue("")
}}
>
Add Cache
Add Key
</Button>
<Button
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,
@@ -459,7 +858,7 @@ const CacheView = memo((props) => {
overflowX: "auto",
}}>
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null, display: "table-row"}}>
{["Key", "Value", "Actions", "Updated"].map((header, index) => (
{["Key", "Value", "Actions", "Updated", "Distribution"].map((header, index) => (
<ListItemText
key={index}
primary={header}
@@ -483,7 +882,7 @@ const CacheView = memo((props) => {
backgroundColor: "#212121",
}}
>
{Array(4)
{Array(5)
.fill()
.map((_, colIndex) => (
<ListItemText
@@ -507,16 +906,39 @@ const CacheView = memo((props) => {
</ListItem>
))
: listCache?.length === 0 ? (
<Typography style={{ textAlign: "center", marginTop: 20, marginBottom: 20, minWidth: 1000, }}>
No Keys Found
</Typography>
<ListItem style={{ display: "table-row" }}>
{Array(5).fill().map((_, index) => (
<ListItemText
key={index}
style={{
display: "table-cell",
padding: "8px",
textAlign: index === 0 ? "center" : "left",
}}
primary={index === 2 ? "No key found." : null}
colSpan={index === 0 ? 5 : undefined}
/>
))}
</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";
}
const validate = validateJson(data.value);
const isDistributed = data?.suborg_distribution?.length > 0 ? true : false;
return (
<ListItem key={index} style={{display:'table-row', backgroundColor: bgColor, maxHeight: 300, overflow: "auto", borderBottomLeftRadius: listCache?.length - 1 === index ? 8 : 0, borderBottomRightRadius: listCache?.length - 1 === index ? 8 : 0,}}>
<ListItemText
@@ -524,7 +946,9 @@ const CacheView = memo((props) => {
display: "table-cell",
overflow: "hidden",
verticalAlign: "middle",
padding: "8px 8px 8px 15px"
padding: "8px 8px 8px 15px",
maxWidth: 200,
overflowX: "auto",
}}
primary={data.key}
/>
@@ -576,13 +1000,14 @@ const CacheView = memo((props) => {
primary={(
<span style={{ display: "inline" }}>
<Tooltip
title="Edit item"
title={data?.org_id !== selectedOrganization.id ? "You can not edit this cache as it is controlled by parent organization." : "Edit this key" }
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
setEditCache(true)
setDataValue({
@@ -598,14 +1023,14 @@ const CacheView = memo((props) => {
</span>
</Tooltip>
<Tooltip
title={"Public URL (types: text, raw, json)"}
title={data?.org_id !== selectedOrganization.id ? "You can not access public URL for this key as it is controlled by parent organization." : "Public URL (types: text, raw, json)" }
style={{ marginLeft: 0, }}
aria-label={"Public URL"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" ? true : false}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
}}
@@ -616,18 +1041,40 @@ const CacheView = memo((props) => {
</span>
</Tooltip>
<Tooltip
title={"Delete item"}
title={data?.org_id !== selectedOrganization.id ? "You can not delete this key as it is controlled by parent organization." : "Delete this key" }
aria-label={"Delete"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
deleteCache(orgId, data.key);
//deleteFile(orgId);
}}
>
<img src="/icons/deleteIcon.svg" alt="delete" />
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
style={{
stroke: data.org_id === selectedOrganization.id ? "#fd4c62" : "#c8c8c8",
}}
>
<path
d="M5 7.20001H6.6H19.4"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M17.7996 7.2V18.4C17.7996 18.8243 17.631 19.2313 17.331 19.5314C17.0309 19.8314 16.624 20 16.1996 20H8.19961C7.77526 20 7.3683 19.8314 7.06824 19.5314C6.76818 19.2313 6.59961 18.8243 6.59961 18.4V7.2M8.99961 7.2V5.6C8.99961 5.17565 9.16818 4.76869 9.46824 4.46863C9.7683 4.16857 10.1753 4 10.5996 4H13.7996C14.224 4 14.6309 4.16857 14.931 4.46863C15.231 4.76869 15.3996 5.17565 15.3996 5.6V7.2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</IconButton>
</span>
</Tooltip>
@@ -642,6 +1089,49 @@ const CacheView = memo((props) => {
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
{selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ?
<ListItemText
primary={
<Tooltip
title="Parent organization controlled datastore. You can use, but not modify this key. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
}
style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
/>
:
<ListItemText
primary={
<Tooltip
title="Distributed to sub-organizations. This means the sub organizations can use this datastore key, but can not modify it."
placement="top"
>
<Checkbox
disabled={ userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
checked={isDistributed}
style={{ margin: "auto" }}
color="secondary"
onClick={() => {
setShowDistributionPopup(true)
if(data?.suborg_distribution?.length > 0){
setSelectedSubOrg(data.suborg_distribution)
}else{
setSelectedSubOrg([])
}
setSelectedCacheKey(data.key)
}}
/>
</Tooltip>
}
style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
/>
}
</ListItem>
);
})}
@@ -653,4 +1143,4 @@ const CacheView = memo((props) => {
);
});
export default memo(CacheView);
export default memo(CacheView);
+780
View File
@@ -0,0 +1,780 @@
import React, { useEffect, useState, useContext } from "react";
import {
FormControl,
Card,
Tooltip,
Typography,
TextField,
Button,
Grid,
ListItem,
ListItemText,
ListItemAvatar,
IconButton,
Avatar,
Zoom,
InputAdornment,
Switch,
Skeleton
} from "@mui/material";
import { ToastContainer, toast } from "react-toastify";
import {
Edit as EditIcon,
Polyline as PolylineIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
} from "@mui/icons-material";
import theme from "../theme.jsx";
import { styled } from '@mui/styles';
import { Context } from "../context/ContextApi.jsx";
const CloudSyncTab = (props) => {
const {
userdata,
globalUrl,
serverside
} = props;
const [cloudSyncApikey, setCloudSyncApikey] = useState("");
const [loading, setLoading] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
const [orgSyncResponse, setOrgSyncResponse] = React.useState("");
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
const [selectedOrganization, setSelectedOrganization] = React.useState({});
const [selectedStatus, setSelectedStatus] = React.useState([]);
const [orgRequest, setOrgRequest] = React.useState(true);
const [userSettings, setUserSettings] = React.useState({});
const [, forceUpdate] = React.useState();
const itemColor = "white";
const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io";
useEffect(() => { getSettings(); }, []);
const GridItem = (props) => {
const [expanded, setExpanded] = React.useState(false);
const [showEdit, setShowEdit] = React.useState(false);
const [newValue, setNewValue] = React.useState(-100);
const primary = props.data.primary;
const secondary = props.data.secondary;
const primaryIcon = props.data.icon;
const secondaryIcon = props.data.active ?
<CheckCircleIcon style={{ color: "green" }} />
:
<CloseIcon style={{ color: "red" }} />
const submitFeatureEdit = (sync_features) => {
if (!userdata.support) {
console.log("User does not have support access and can't edit features");
return
}
sync_features.editing = true
const data = {
org_id: selectedOrganization.id,
sync_features: sync_features,
};
console.log("sync_features: ", sync_features);
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed updating org: ", responseJson.reason);
} else {
toast("Successfully edited org!");
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
}
const enableFeature = () => {
console.log("Enabling " + primary)
console.log(selectedOrganization.sync_features)
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_")
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: " + tmpprimary)
return
}
if (props.data.active) {
selectedOrganization.sync_features[tmpprimary].active = false
} else {
selectedOrganization.sync_features[tmpprimary].active = true
}
setSelectedOrganization(selectedOrganization)
forceUpdate(Math.random())
submitFeatureEdit(selectedOrganization.sync_features)
}
const submitEdit = (e) => {
e.preventDefault();
e.stopPropagation();
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_")
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: " + tmpprimary)
return
}
// Make it into a number
var tmp = parseInt(newValue)
if (isNaN(tmp)) {
console.log("Not a number: " + newValue)
return
}
selectedOrganization.sync_features[tmpprimary].limit = tmp
setSelectedOrganization(selectedOrganization)
forceUpdate(Math.random())
submitFeatureEdit(selectedOrganization.sync_features)
}
const handleToggleFeature = (e) => {
// Your logic for toggling the feature's active state
console.log(`Toggling ${primary}`);
if (!isCloud || userdata.support !== true) {
return
}
e.preventDefault();
e.stopPropagation();
enableFeature()
};
return (
<Grid
item
xs={4}
>
<div
style={{
margin: 4,
backgroundColor: "#1a1a1a",
borderRadius: 8,
color: "white",
minHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 300 : "inherit",
boxShadow: "none",
}}
>
<ListItem
style={{ cursor: "pointer", }}
onClick={() => {
setExpanded(prev => !prev);
if(showEdit){
setShowEdit(false)
}
}}
>
<ListItemAvatar>
<Avatar>{primaryIcon}</Avatar>
</ListItemAvatar>
<ListItemText
style={{ textTransform: "capitalize", color: "#F1F1F1", fontSize: 14, fontWeight: 400, }}
primary={primary}
/>
{isCloud && userdata.support === true ?
<Tooltip title="Edit features (support users only)">
<EditIcon
color="secondary"
style={{ marginRight: 10, cursor: "pointer", }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('expanded', expanded)
if (expanded){
setExpanded(false)
}
if (showEdit) {
setShowEdit(false)
return
}
console.log("Edit")
setShowEdit(true)
}}
/>
</Tooltip>
: null}
{userdata.support === true ?(
<Tooltip title={props.data.active ? 'Disable feature' : 'Enable feature'}>
<Switch
checked={props.data.active}
onChange={handleToggleFeature}
color="primary"
inputProps={{ 'aria-label': 'feature toggle' }}
sx={{
'& .MuiSwitch-switchBase.Mui-checked': {
color: '#FFFFFF',
},
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
backgroundColor: props.data.active ? '#2BC07E' : "#9e9e9e",
},
"& .MuiSwitch-track": {
backgroundColor: props.data.active ? '#2BC07E' : "#9e9e9e"
}
}}
/>
</Tooltip>
):(
<Tooltip title={props.data.active ? "Disable feature" : "Enable feature"}>
<span
style={{ cursor: "pointer", marginTop: 5, }}
onClick={(e) => {
if (!isCloud || userdata.support !== true) {
return
}
e.preventDefault();
e.stopPropagation();
enableFeature()
}}
>
{secondaryIcon}
</span>
</Tooltip>
)}
</ListItem>
{expanded ?
<div style={{ padding: 15 }}>
<Typography>
<b>Usage:&nbsp;</b>
{props.data.limit === 0 ? (
"Unlimited"
) : (
<span>
{props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit}
</span>
)}
</Typography>
{/*<Typography>
Data sharing: {props.data.data_collection}
</Typography>*/}
<Typography style={{ maxHeight: 150, overflowX: "hidden", overflowY: "auto" }}><b>Description:</b> {secondary}</Typography>
</div>
: null}
{showEdit ?
<FormControl fullWidth onSubmit={(e) => {
console.log("Submit")
submitEdit(e)
}}>
<span style={{ display: "flex",}}>
<TextField
style={{ flex: 3, }}
color="primary"
label={"Edit value"}
defaultValue={props.data.limit}
sx={{
marginTop: 0.5,
marginBottom: 0.5
}}
onChange={(event) => {
setNewValue(event.target.value)
}}
/>
<Button
style={{ flex: 1, }}
variant="contained"
disabled={newValue < -1}
onClick={(e) => {
console.log("Submit 2")
submitEdit(e)
}}
sx={{
marginTop: 0.5,
maarginBottom: 0.5
}}
>
Submit
</Button>
</span>
</FormControl>
: null}
</div>
</Grid>
);
};
const handleGetOrg = (orgId) => {
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const foundorgid = params["org_id"];
if (foundorgid !== undefined && foundorgid !== null) {
orgId = foundorgid;
}
}
if (orgId.length === 0) {
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.");
return;
}
// Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 401) {
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed getting your org. If this persists, please contact support.");
} else {
if (
responseJson.sync_features === undefined ||
responseJson.sync_features === null
) {
responseJson.sync_features = {};
}
setSelectedOrganization(responseJson)
var lists = {
active: {
triggers: [],
features: [],
sync: [],
},
inactive: {
triggers: [],
features: [],
sync: [],
},
};
setOrganizationFeatures(lists);
}
})
.catch((error) => {
console.log("Error getting org: ", error);
toast("Error getting current organization");
});
};
const handleStopOrgSync = (org_id) => {
if (org_id === undefined || org_id === null) {
toast("Couldn't get org " + org_id);
return;
}
const data = {};
const url = globalUrl + "/api/v1/orgs/" + org_id + "/stop_sync";
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status === 200) {
console.log("Cloud sync success?");
toast("Successfully stopped cloud sync");
} else {
console.log("Cloud sync fail?");
toast(
"Failed stopping sync. Try again, and contact support if this persists."
);
}
return response.json();
})
.then((responseJson) => {
setTimeout(() => {
handleGetOrg(org_id);
}, 1000);
})
.catch((error) => {
toast("Err: " + error.toString());
});
};
const enableCloudSync = (apikey, organization, disableSync) => {
setOrgSyncResponse("");
const data = {
apikey: apikey,
organization: organization,
disable: disableSync,
};
const url = globalUrl + "/api/v1/cloud/setup";
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) => {
setLoading(false);
if (response.status === 200) {
console.log("Cloud sync success?");
} else {
console.log("Cloud sync fail?");
}
return response.json();
//setTimeout(() => {
//}, 1000)
})
.then((responseJson) => {
console.log("RESP: ", responseJson);
if (
responseJson.success === false &&
responseJson.reason !== undefined
) {
setOrgSyncResponse(responseJson.reason);
toast("Failed to handle sync: " + responseJson.reason);
} else if (!responseJson.success) {
toast("Failed to handle sync.");
} else {
//getOrgs(); API no longer in use, as it's in handleInfo request
if (disableSync) {
toast("Successfully disabled sync!");
setOrgSyncResponse("Successfully disabled syncronization");
} else {
toast("Cloud Syncronization successfully set up!");
setOrgSyncResponse(
"Successfully started syncronization. Cloud features you now have access to can be seen below."
);
}
selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync;
setSelectedOrganization(selectedOrganization);
setCloudSyncApikey("");
handleGetOrg(userdata.active_org.id);
}
})
.catch((error) => {
setLoading(false);
toast("Err: " + error.toString());
});
};
const getSettings = () => {
fetch(globalUrl + "/api/v1/getsettings", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 when getting settings :O!");
}
return response.json();
})
.then((responseJson) => {
setUserSettings(responseJson);
})
.catch((error) => {
console.log(error);
});
};
if (
selectedOrganization.id === undefined &&
userdata !== undefined &&
userdata.active_org !== undefined &&
orgRequest === true
) {
setOrgRequest(false);
handleGetOrg(userdata.active_org.id);
}
return (
<div style={{padding: "27px 10px 19px 27px",}}>
<div style={{ marginBottom: 20 }}>
<h2
style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}
>
Cloud syncronization
</h2>
<span style={{ color: "#C8C8C8", fontSize: 16, fontWeight: 400, }}>
What does <a href="/docs/organizations#cloud_sync" target="_blank" rel="noopener noreferrer" style={{ color: "rgba(255, 132, 68, 1)", fontSize: 16, textDecoration: 'none', }}>cloud sync</a> do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach.
</span>
</div>
{isCloud ? (
<div style={{ marginTop: 15, display: "flex" }}>
<div style={{ flex: 1 }}>
<Typography style={{fontWeight: 400, fontSize: 16, color: "#F1F1F1"}}>
Currently syncronizing:{" "}
{selectedOrganization.cloud_sync_active === true
? <span style={{ color: "#4CFD72", fontSize: 16, marginLeft: 16}}>True</span>
: <span style={{ color: "#FD4C62", fontSize: 16, marginLeft: 16 }}>False</span>}
</Typography>
{selectedOrganization.cloud_sync_active ? (
<Typography style={{}}>
Syncronization interval:{" "}
{selectedOrganization.sync_config.interval === 0
? "60"
: selectedOrganization.sync_config.interval}
</Typography>
) : null}
<Typography
style={{
whiteSpace: "nowrap",
marginTop: 25,
marginRight: 10,
fontSize: 16,
fontWeight: 400,
fontFamily: theme.typography.fontFamily,
}}
>
Your Api key
</Typography>
{userSettings?.apikey === undefined || userSettings?.apikey === null || userSettings?.apikey?.length <=0 ? (
<Skeleton variant="rectangular" animation="wave" sx={{backgroundColor: '#212121', border: '1px solid #646464', width: 500, height: 50, marginTop: 2 }}/>
):
<div style={{ display: "flex" }}>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
maxWidth: 500,
height: 35
}}
InputProps={{
sx: {
height: "35px",
color: "white",
fontSize: "1em",
backgroundColor: '#212121',
},
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setShowApiKey(!showApiKey)
}}
>
{showApiKey ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
)
}}
required
fullWidth={true}
disabled={true}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
value={userSettings?.apikey}
defaultValue={userSettings?.apikey}
type={!isCloud || showApiKey ? "text" : "password"}
/>
{selectedOrganization.cloud_sync_active ? (
<Button
style={{
width: 150,
height: 50,
marginLeft: 10,
marginTop: 17,
}}
variant={
selectedOrganization.cloud_sync_active === true
? "outlined"
: "contained"
}
color="primary"
onClick={() => {
handleStopOrgSync(selectedOrganization.id);
}}
>
Stop Sync
</Button>
) : null}
</div>}
</div>
</div>
) : (
<div>
<div style={{ display: "flex", marginBottom: 20 }}>
<TextField
color="primary"
style={{
backgroundColor: "#1a1a1a",
marginRight: 10,
height: 35,
}}
InputProps={{
style: {
height: "35px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
disabled={selectedOrganization.cloud_sync}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
onChange={(event) => {
setCloudSyncApikey(event.target.value);
}}
/>
<Button
disabled={
(!selectedOrganization.cloud_sync &&
cloudSyncApikey.length === 0) ||
loading
}
style={{ marginTop: 15, height: 35, width: 150, textTransform: 'none', fontSize: 16, color: (!selectedOrganization.cloud_sync &&
cloudSyncApikey.length === 0) ||
loading ? null : "#1a1a1a", backgroundColor: (!selectedOrganization.cloud_sync &&
cloudSyncApikey.length === 0) ||
loading? null : "#FF8544" }}
onClick={() => {
setLoading(true);
enableCloudSync(
cloudSyncApikey,
selectedOrganization,
selectedOrganization.cloud_sync
);
}}
color="primary"
variant={
selectedOrganization.cloud_sync === true
? "outlined"
: "contained"
}
>
{selectedOrganization.cloud_sync
? "Stop sync"
: "Start sync"}
</Button>
</div>
{orgSyncResponse.length > 0 ? (
<Typography style={{ marginTop: 5, marginBottom: 10 }}>
Message from Shuffle Cloud: <b>{orgSyncResponse}</b>
</Typography>
) : null}
</div>
)}
<h2 style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}>
Features
</h2>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 24, fontSize: 16, fontWeight: 400, marginLeft: 5, color: "#C8C8C8" }}>
Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. </Typography>
<Grid container style={{ width: "100%", marginBottom: 15, }}>
{selectedOrganization.sync_features === undefined ||
selectedOrganization.sync_features === null
? <Grid container spacing={2} justifyContent="center">
{[...Array(18)].map((_, i) => (
<Grid item xs={12} sm={6} md={4} key={i}>
<div
style={{
margin: 4,
borderRadius: 8,
minHeight: "inherit",
maxHeight: "inherit",
boxShadow: "none",
display: 'flex',
justifyContent: 'center',
}}
>
<Skeleton
variant="rectangular"
height={50}
width={343}
sx={{ backgroundColor: '#1a1a1a', display: 'flex', borderRadius: 1 }}
animation="wave"
/>
</div>
</Grid>
))}
</Grid>
: Object.keys(selectedOrganization.sync_features).map(function (
key,
index
) {
if (key === "schedule" || key === "apps" || key === "updates" || key === "editing") {
return null;
}
const item = selectedOrganization.sync_features[key];
if (item === null) {
return null
}
const newkey = key.replaceAll("_", " ");
const griditem = {
primary: newkey,
secondary:
item.description === undefined ||
item.description === null ||
item.description.length === 0
? "Not defined yet"
: item.description,
limit: item.limit,
usage: item.usage === undefined ||
item.usage === null ? 0 : item.usage,
data_collection: "None",
active: item.active,
icon: <PolylineIcon style={{ color: "#1a1a1a" }} />,
};
return (
<Zoom key={index}>
<GridItem data={griditem} />
</Zoom>
);
})}
</Grid>
</div>
);
};
export default CloudSyncTab;
+16 -4
View File
@@ -799,12 +799,16 @@ const ConfigureWorkflow = (props) => {
>
<div
style={{
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: "100%", padding: 12, cursor: "pointer",
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: "100%", padding: 12, cursor: filled ? "default" : "pointer",
}}
id="app-config"
>
<div style={{display: "flex", }}
onClick={() => {
if (filled) {
return
}
setOpened(!opened);
// Scroll to it
@@ -865,6 +869,7 @@ const ConfigureWorkflow = (props) => {
isLoggedIn={true}
getAppAuthentication={undefined}
workflow={workflow}
setFinalized={setFinalized}
/>
</div>
@@ -1377,7 +1382,7 @@ const ConfigureWorkflow = (props) => {
return (
<div>
<div style={{margin: setConfigureWorkflowModalOpen !== undefined ? "0px 50px 0px 50px" : "35px 0px 0px 0px", maxHeight: 475, }}>
<div style={{margin: setConfigureWorkflowModalOpen !== undefined ? "0px 50px 0px 50px" : "25px 0px 0px 0px", maxHeight: 475, }}>
{setConfigureWorkflowModalOpen !== undefined ?
@@ -1387,7 +1392,7 @@ const ConfigureWorkflow = (props) => {
: null
}
<div style={{marginTop: 10, }} />
<div style={{marginTop: setConfigureWorkflowModalOpen !== undefined ? 10 : 0, }} />
{/*
<WorkflowValidationTimeline
@@ -1404,7 +1409,7 @@ const ConfigureWorkflow = (props) => {
{requiredActions.length > 0 ? (
<span>
<Typography variant="body2" color="textSecondary">
Please configure the following steps to help us complete your workflow. This can also be done later.
To complete the workflow setup, please configure the following steps.
</Typography>
{setConfigureWorkflowModalOpen !== undefined ?
@@ -1434,6 +1439,13 @@ const ConfigureWorkflow = (props) => {
)
})}
</List>
{/*
<Typography variant="body2" color="textSecondary">
Once done, you may continue to the workflow.
</Typography>
*/}
</span>
) : null}
@@ -36,7 +36,8 @@ const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isD
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
//const url = `${globalUrl}/api/v1/detections/${detectionType}/selected_rules/${action}`;
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
fetch(url, {
method: "PUT",
@@ -258,7 +259,7 @@ const DetectionExplorer = (props) => {
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
)
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.data_lake?.enabled === true).length : 0
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
return (
<Container>
@@ -284,7 +285,8 @@ const DetectionExplorer = (props) => {
</Typography>
<div style={{display: "flex", }}>
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
{/*workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
<div style={{display: "flex", }}>
<div style={{minWidth: 400, maxWidth: 400, }}>
<WorkflowValidationTimeline
@@ -313,7 +315,7 @@ const DetectionExplorer = (props) => {
</IconButton>
</div>
:
: */}
<Button
variant="contained"
onClick={() => {
@@ -331,11 +333,11 @@ const DetectionExplorer = (props) => {
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
</Button>
}
{/**/}
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
<a href="/admin?tab=environments" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<a href="/admin?tab=Locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
</a>
</Tooltip>
@@ -137,6 +137,8 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
setResponseValue(e.target.value)
toast.error("The automatic response system is NOT available for you yet. Please contact support@shuffler.io if you want to try this feature.")
// FIXME: Handle:
// 1. Get the current cache for the detection
// 2. Create a new mapping for Detection -> Response
@@ -187,7 +189,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
<Switch
checked={isEnabled && !folderDisabled}
onChange={handleSwitchChange}
disabled={false}
disabled={true}
/>
</Tooltip>
</div>
+446
View File
@@ -0,0 +1,446 @@
import React, { useEffect, useState, useContext } from 'react';
import OrgHeaderexpanded from "../components/OrgHeaderexpandedNew.jsx";
import OrgHeader from '../components/OrgHeaderNew.jsx';
import { toast } from "react-toastify";
import CloudSyncTab from '../components/CloudSyncTab.jsx';
import {
FileCopy as FileCopyIcon,
} from "@mui/icons-material";
import {
Button,
Tooltip,
IconButton,
} from "@mui/material";
const EditOrgTab = (props) => {
const {
userdata,
globalUrl,
serverside,
selectedOrganization,
setSelectedOrganization,
handleGetOrg,
selectedStatus, setSelectedStatus,
handleEditOrg,
} = props;
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
const [users, setUsers] = React.useState([]);
const [orgRequest, setOrgRequest] = React.useState(true);
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
useEffect(() => {
if(users.length === 0) {
getUsers();
}
}, []);
const handleStatusChange = (event) => {
const { value } = event.target;
setSelectedStatus(value);
handleEditOrg(
selectedOrganization?.name,
selectedOrganization?.description,
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: selectedOrganization?.defaults?.newsletter,
weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
},
{
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,
},
value.length === 0 ? ["none"] : value,
);
};
const getUsers = () => {
fetch(globalUrl + "/api/v1/getusers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
// Ahh, this happens because they're not admin
// window.location.pathname = "/workflows"
return;
}
return response.json();
})
.then((responseJson) => {
setUsers(responseJson);
})
.catch((error) => {
toast(error.toString());
});
};
const mailsendingButton = (org) => {
if (org === undefined || org === null) {
return ""
}
if (users.length === 0) {
return ""
}
// 1 mail based on users that have only apps
// Another based on those doing workflows
// Another based on those trying usecases(?) or templates
//
// Start based on edr, siem & ticketing
// Talk about enrichment?
// Check suggested usecases
// Check suggested workflows
var your_apps = "- Connecting "
var subject_add = 0
var subject = "POC to automate "
if (org.security_framework !== undefined && org.security_framework !== null) {
if (org.security_framework.cases.name !== undefined && org.security_framework.cases.name !== null && org.security_framework.cases.name !== "") {
your_apps += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1) + ", "
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and "
}
subject_add += 1
subject += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1)
}
}
if (org.security_framework.siem.name !== undefined && org.security_framework.siem.name !== null && org.security_framework.siem.name !== "") {
your_apps += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1) + ", "
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and "
}
subject_add += 1
subject += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1)
}
}
if (org.security_framework.communication.name !== undefined && org.security_framework.communication.name !== null && org.security_framework.communication.name !== "") {
your_apps += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1) + ", "
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and "
}
subject_add += 1
subject += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1)
}
}
if (org.security_framework.edr.name !== undefined && org.security_framework.edr.name !== null && org.security_framework.edr.name !== "") {
your_apps += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1) + ", "
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and "
}
subject_add += 1
subject += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1)
}
}
if (org.security_framework.intel.name !== undefined && org.security_framework.intel.name !== null && org.security_framework.intel.name !== "") {
your_apps += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1) + ", "
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and "
}
subject_add += 1
subject += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1)
}
}
// Remove comma
//subject += "?"
your_apps = your_apps.substring(0, your_apps.length - 2)
}
// Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase
var usecases = "- Building usecases like "
const active_usecase = org.priorities.filter((item) => item.type === "usecase" && item.active === true)
if (active_usecase.length > 0) {
for (var i = 0; i < active_usecase.length; i++) {
if (active_usecase[i].name.includes("Suggested Usecase: ")) {
usecases += active_usecase[i].name.replace("Suggested Usecase: ", "", -1) + ", "
} else {
usecases += active_usecase[i].name + ", "
}
}
usecases = usecases.substring(0, usecases.length - 2)
}
if (your_apps.length <= 15) {
your_apps = ""
}
if (usecases.length <= 30) {
usecases = ""
}
var workflow_amount = "a few"
var admins = ""
// Loop users
var lastLogin = 0
for (var i = 0; i < users.length; i++) {
if (users[i].username.includes("shuffler")) {
continue
}
if (users[i].role === "admin") {
admins += users[i].username + ","
}
const data = users[i]
for (var i = 0; i < data.login_info.length; i++) {
if (data.login_info[i].timestamp > lastLogin) {
lastLogin = data.login_info[i].timestamp
}
}
}
// Remove last comma
admins = admins.substring(0, admins.length - 1)
if (your_apps.length > 5) {
your_apps += "%0D%0A"
}
if (usecases.length > 5) {
usecases += "%0D%0A"
}
// Get drift username from userdata.username before @ in email
const username = userdata.username.substring(0, userdata.username.indexOf("@"))
// Check if timestamp is more than 2 weeks ago and add "a while back" to the message
const timeComparison = 1209600
const extra_timestamp_text = lastLogin === 0 ? 0 : (Date.now() / 1000 - lastLogin) > timeComparison ? " a while back" : ""
console.log("LAST LOGIN: " + lastLogin, extra_timestamp_text)
// Check if cloud sync is active, and if so, add a message about it
const cloudSyncInfo = selectedOrganization.cloud_sync === true ? "- Scale your onprem installation" : ""
var body = `Hey,%0D%0A%0D%0AI noticed you tried to use Shuffle${extra_timestamp_text}, and thought you may be interested in a POC. It looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting what you wanted out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A
Some of the things we can help with:%0D%0A
${your_apps}
- Configuring and authenticating your apps%0D%0A
${usecases}
- Multi-Tenancy and creating special usecases%0D%0A
${cloudSyncInfo}%0D%0A
If you're interested, please let me know a time that works for you, or set up a call here: https://drift.me/${username}`
return `mailto:${admins}?bcc=frikky@shuffler.io,binu@shuffler.io&subject=${subject}&body=${body}`
}
if (
selectedOrganization.id === undefined &&
userdata !== undefined &&
userdata.active_org !== undefined &&
orgRequest
) {
setOrgRequest(false);
}
return (
<div style={{ width: "100%", boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: '#212121', borderRadius: '16px', }}>
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}} >
<div style={{ marginBottom: 20 }}>
<div style={{display:"flex"}}>
<div style={{width:'70%'}}>
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Organization overview</h2>
<span style={{ color: "#9E9E9E" }}>
On this page organization admins can configure organisations, and sub-orgs (MSSP).{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#organization"
style={{ color: "#FF8444" }}
>
Learn more
</a>
</span>
</div>
<div style={{display:"flex", alignItems:"center", marginLeft:50}}>
<Tooltip
title="Copy Organization ID"
aria-label="Copy orgid"
>
<IconButton
style={{
display: "flex",
alignItems: "center",
width: 40,
height: 40,
backgroundColor: "rgba(47, 47, 47, 1)",
borderRadius: 200
}}
onClick={() => {
const org_id = selectedOrganization.id;
// Check if organization ID exists
if (!org_id) {
toast("No organization ID found");
return;
}
// Use clipboard API
navigator.clipboard.writeText(org_id)
.then(() => {
toast.success(`${org_id} copied to clipboard`);
})
.catch((error) => {
// Fallback for browsers that don't support clipboard API
try {
// Create temporary input element
const tempInput = document.createElement('input');
tempInput.value = org_id;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
toast(`${org_id} copied to clipboard`);
} catch (err) {
toast("Failed to copy. Please try again.");
console.error("Copy failed:", err);
}
});
}}
>
<FileCopyIcon style={{ color: "rgba(255,255,255,0.8)" }} />
</IconButton>
</Tooltip>
{userdata.support === true ?
<span style={{ display: "flex", alignItems: "center", marginLeft:16 }}>
{/*<a href={mailsendingButton(selectedOrganization)} target="_blank" rel="noopener noreferrer" style={{textDecoration: "none"}} disabled={selectedStatus.length !== 0}>*/}
<Button
// variant="outlined"
// color="primary"
// disabled={selectedStatus.length !== 0}
style={{
width: 180,
height: 40,
borderRadius: 4,
border: "1.5px solid #ff8544",
background: "transparent",
color: "#ff8544",
fontSize: 16,
textTransform: "none",
boxShadow: "none",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => {
console.log("Should send mail to admins of org with context")
handleStatusChange({ target: { value: ["contacted"] } })
// Open a new tab
window.open(mailsendingButton(selectedOrganization), "_blank")
}}
>
Sales mail
</Button>
</span>
: null}
</div>
</div>
{/* {isCloud ?
<Tooltip
title={`Your organization is in ${regiontag}. Click to change!`}
style={{
}}
>
<Avatar
style={{ cursor: "pointer", top: -10, right: 50, position: "absolute", }}
onClick={() => {
if (userdata.support === false) {
toast("Region change is not directly implemented yet, and requires support help.")
if (window.drift !== undefined) {
window.drift.api.startInteraction({
interactionId: 386411,
})
}
} else {
// Show region change modal
console.log("Should open region change modal")
setRegionChangeModalOpen(true)
}
}}
>
{regiontag}
</Avatar>
</Tooltip>
: null} */}
</div>
<OrgHeader
isCloud={isCloud}
userdata={userdata}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
handleEditOrg={handleEditOrg}
isEditOrgTab={true}
handleGetOrg={handleGetOrg}
/>
<OrgHeaderexpanded
isCloud={isCloud}
userdata={userdata}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
isEditOrgTab={true}
handleGetOrg={handleGetOrg}
serverside={serverside}
/>
</div>
</div >
)
}
export default EditOrgTab;
+216 -130
View File
@@ -71,7 +71,7 @@ const EditWorkflow = (props) => {
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [submitLoading, setSubmitLoading] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(expanded === true ? true : false);
const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false);
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
@@ -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();
@@ -103,9 +105,10 @@ const EditWorkflow = (props) => {
const foundScroll = document.getElementById(scrollTo)
if (foundScroll !== null) {
// Smooth scroll
foundScroll.scrollIntoView({ behavior: "smooth" })
foundScroll.scrollIntoView({
behavior: "smooth",
})
}
}, 200)
setScrollDone(true)
@@ -188,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"}
@@ -244,15 +251,13 @@ const EditWorkflow = (props) => {
Workflows can be built from scratch, or from templates. <a href="/usecases2" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
</Typography>
{/*
<div style={{marginTop: 10, marginBottom: 10, marginRight: 50, }}>
<WorkflowValidationTimeline
apps={apps}
workflow={workflow}
/>
</div>
*/}
<div style={{marginTop: 10, marginBottom: 10, marginRight: 50, }}>
<WorkflowValidationTimeline
apps={apps}
workflow={workflow}
/>
</div>
{showUpload === true ?
<div style={{ float: "right" }}>
@@ -290,7 +295,7 @@ const EditWorkflow = (props) => {
bottom: 0,
zIndex: 1002,
backgroundColor: theme.palette.backgroundColor,
height: 50,
height: 75,
paddingTop: 20,
paddingLeft: 75,
}}>
@@ -324,15 +329,21 @@ 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
if (newWorkflowTags.length > 0) {
innerWorkflow.tags = newWorkflowTags
} else {
innerWorkflow.tags = []
}
if (selectedUsecases.length > 0) {
innerWorkflow.usecase_ids = selectedUsecases
} else {
innerWorkflow.usecase_ids = []
}
if (dueDate > 0) {
@@ -361,7 +372,6 @@ const EditWorkflow = (props) => {
setWorkflow({})
} else {
setWorkflow(innerWorkflow)
console.log("editing workflow: ", innerWorkflow)
}
setSubmitLoading(true)
@@ -505,7 +515,7 @@ const EditWorkflow = (props) => {
color: "white",
},
}}
multiLine
multiline
rows={3}
color="primary"
defaultValue={innerWorkflow.description}
@@ -537,81 +547,10 @@ const EditWorkflow = (props) => {
</RadioGroup>
</FormControl>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
sx={{
marginTop: 3,
marginLeft: 3,
}}
value={dueDate}
label="Due Date"
format="YYYY-MM-DD"
onChange={(newValue) => {
setDueDate(newValue)
}}
/>
</LocalizationProvider>
</div>
<div />
<FormControl style={{ marginTop: 15, }}>
<FormLabel id="demo-row-radio-buttons-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="row-radio-buttons-group"
defaultValue={innerWorkflow.workflow_type}
onChange={(e) => {
console.log("Data: ", e.target.value)
innerWorkflow.workflow_type = e.target.value
setInnerWorkflow(innerWorkflow)
}}
>
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
</RadioGroup>
</FormControl>
<TextField
onBlur={(event) => {
innerWorkflow.blogpost = event.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={innerWorkflow.blogpost}
placeholder="A blogpost or other reference for how this work workflow was built, and what it's for."
rows="1"
label="blogpost"
margin="dense"
fullWidth
/>
<TextField
onBlur={(event) => {
innerWorkflow.video = event.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={innerWorkflow.video}
placeholder="A youtube or loom link to the video"
rows="1"
label="Video"
margin="dense"
fullWidth
/>
<TextField
onBlur={(event) => {
@@ -633,21 +572,21 @@ const EditWorkflow = (props) => {
fullWidth
/>
<Divider style={{ marginTop: 20, marginBottom: 20, }} />
<Divider id="mssp_control" style={{ marginTop: 20, marginBottom: 20, }} />
<Typography variant="h4" style={{ marginTop: 50, }}>
MSSP controls
Multi-Tenancy, Backups & Security
</Typography>
<Typography variant="body1" style={{ marginTop: 50, }}>
MSSP Suborg Distribution (<b>beta</b> - contact support@shuffler.io for more info)
<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. (contact support@shuffler.io if you want a demo. Please try it!)
</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)" }}>
Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please <a href="/admin?tab=suborgs" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">make one</a> or get access to suborgs by another admin, then try again.
Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please <a href="/admin?tab=tenants" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">make one</a> or get access to suborgs by another admin, then try again.
</Typography>
:
<Typography variant="body2" style={{ marginTop: 10, color: "rgba(255,255,255,0.7)" }}>
@@ -687,6 +626,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
@@ -694,6 +634,8 @@ const EditWorkflow = (props) => {
return null
}
const correctRegion = data?.region_url === userdata?.region_url
const imagesize = 22
const imageStyle = {
width: imagesize,
@@ -727,7 +669,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 }}>
@@ -738,22 +684,19 @@ const EditWorkflow = (props) => {
})}
</Select>
:
<Link to={"/admin?tab=suborgs"} style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">
<Link to={"/admin?tab=tenants"} style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">
<Typography variant="body2" style={{ marginTop: 10, }}>
Create a sub-org to distribute workflows to suborgs.
</Typography>
</Link>
}
{/*<Divider style={{marginTop: 20, marginBottom: 20, }} />*/}
<Typography variant="body1" style={{ marginTop: 100, }}>
<Typography variant="h6" style={{ marginTop: 75, }}>
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your <a href="/admin?admin_tab=organization" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">default backup repository</a>. <a href="/docs/configuration#environment-variables" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">Credentials are encrypted.</a> Creates <a href="/admin?admin_tab=priorities" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">notifications</a> if it fails.
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' and be formatted, with removed images, to make diffing easy. Overrides your <a href="/admin?admin_tab=org_config" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">default backup repository</a>. <a href="/docs/configuration#environment-variables" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">Credentials are encrypted.</a> Creates <a href="/admin?admin_tab=notifications" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">notifications</a> if it fails.
</Typography>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
@@ -895,6 +838,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, }} />
@@ -1094,11 +1091,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, }}>
@@ -1146,41 +1143,130 @@ const EditWorkflow = (props) => {
</Select>
</FormControl>
</div>
</div>
: null}
{!isEditing ? <>
<div style={{ marginTop: 20, }}>
<FormControlLabel
control={<Checkbox />}
label="Create workflow as code"
style={{ marginTop: '12px' }}
onChange={(e) => {
setWorkflowAsCode(e.target.checked)
workflow.workflow_as_code = e.target.checked
setWorkflow(workflow)
setInnerWorkflow(workflow)
<Divider style={{marginTop: 75, marginBottom: 75, }}/>
<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 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={{
marginTop: 3,
marginLeft: 3,
}}
value={dueDate}
label="Due Date"
format="YYYY-MM-DD"
onChange={(newValue) => {
setDueDate(newValue)
}}
/>
</LocalizationProvider>
</div>
</> : null}
<FormControl style={{ marginTop: 15, }}>
<FormLabel id="demo-row-radio-buttons-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="row-radio-buttons-group"
defaultValue={innerWorkflow.workflow_type}
onChange={(e) => {
console.log("Data: ", e.target.value)
<Tooltip color="primary" title={"Add more details"} placement="top">
<Button
style={{ margin: "auto", marginTop: 50, marginBottom: 10, textAlign: "center", textTransform: "none", }}
variant="outlined"
disabled={newWorkflow === true}
color="secondary"
onClick={() => {
setShowMoreClicked(!showMoreClicked);
innerWorkflow.workflow_type = e.target.value
setInnerWorkflow(innerWorkflow)
}}
>
<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>
<TextField
onBlur={(event) => {
innerWorkflow.blogpost = event.target.value
setInnerWorkflow(innerWorkflow)
}}
>
{showMoreClicked ? <ExpandLessIcon style={{ marginRight: 10, }} /> : <ExpandMoreIcon style={{ marginRight: 10, }} />}
{showMoreClicked ? "Less Options" : "More Options"}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={innerWorkflow.blogpost}
placeholder="A blogpost or other reference for how this work workflow was built, and what it's for."
rows="1"
label="blogpost"
margin="dense"
fullWidth
/>
<TextField
onBlur={(event) => {
innerWorkflow.video = event.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={innerWorkflow.video}
placeholder="A youtube or loom link to the video"
rows="1"
label="Video"
margin="dense"
fullWidth
/>
</Button>
</Tooltip>
<Tooltip color="primary" title={"Add more details"} placement="top">
<Button
style={{ margin: "auto", marginTop: 50, marginBottom: 10, textAlign: "center", textTransform: "none", }}
variant="outlined"
disabled={newWorkflow === true}
color="secondary"
onClick={() => {
setShowMoreClicked(!showMoreClicked);
}}
>
{showMoreClicked ? <ExpandLessIcon style={{ marginRight: 10, }} /> : <ExpandMoreIcon style={{ marginRight: 10, }} />}
{showMoreClicked ? "Less Options" : "More Options"}
</Button>
</Tooltip>
</div>
: null}
</div>
File diff suppressed because it is too large Load Diff
+3 -3
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}
@@ -1567,7 +1567,7 @@ const Files = memo((props) => {
placement="top"
>
<Checkbox
disabled={selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" ? true : false}
disabled={userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" ) ? true : false}
checked={isDistributed}
style={{ }}
color="secondary"
@@ -644,6 +644,7 @@ const FixWorkflowValidationErrors = (props) => {
console.log("Workflow validation: ", workflow.validation)
return (
<div>
{/*
{workflow.errors !== undefined && workflow.errors !== null ?
<div>
General errors: {workflow.errors.length}
@@ -656,11 +657,8 @@ const FixWorkflowValidationErrors = (props) => {
})}
</div>
: null}
<Divider style={{marginTop: 15, marginBottom: 15, }}/>
{workflow.validation.errors !== undefined && workflow.validation.errors !== null ?
workflow.validation.errors !== undefined && workflow.validation.errors !== null ?
<div>
Validation errors: {workflow.validation.errors.length}
{workflow.validation.errors.map((error, index) => {
@@ -675,10 +673,12 @@ const FixWorkflowValidationErrors = (props) => {
)
})}
</div>
: null}
: null*/}
{/*
<Divider style={{marginTop: 15, marginBottom: 15, }} />
Apps loaded: {apps.length}
*/}
</div>
)
+238 -118
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState, useContext, useCallback, useMemo } from "react";
import React, { useEffect, useRef, useState, useContext, useCallback, useMemo, memo } from "react";
import {
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
@@ -7,7 +7,9 @@ import {
Add as AddIcon,
BorderColor,
Close as CloseIcon,
ConstructionOutlined,
ConstructionOutlined as ConstructionOutlinedIcon,
Toc as TocIcon,
Settings as SettingsIcon
} from "@mui/icons-material";
import SearchBox from "./SearchData.jsx";
import {
@@ -27,11 +29,9 @@ import {
Fade,
Portal,
Collapse,
Tooltip,
} from "@mui/material";
import theme from "../theme.jsx";
import {
Settings as SettingsIcon
} from "@mui/icons-material";
import RecentWorkflow from "../components/RecentWorkflow.jsx";
import { useNavigate } from "react-router";
@@ -143,8 +143,6 @@ useEffect(() => {
>
<Box
sx={{
maxHeight: 250,
overflowY: "auto",
scrollbarWidth: "thin",
scrollbarColor: "#494949 transparent",
"& .MuiAutocomplete-listbox": {
@@ -238,7 +236,7 @@ useEffect(() => {
setOpenautomateTab(true);
setOpenSecurityTab(false);
setCurrentOpenTab("workflows");
} else if ((lastTabOpenByUser === "apps" && currentPath.includes("/search")) || currentPath.includes("/search")) {
} else if ((lastTabOpenByUser === "apps" && currentPath.includes("/apps")) || currentPath.includes("/apps")) {
setOpenautomateTab(true);
setOpenSecurityTab(false);
setCurrentOpenTab("apps");
@@ -500,16 +498,6 @@ useEffect(() => {
})
</MenuItem>
</Link>
<Link to="/usecases" style={hrefStyle}>
<MenuItem
onClick={(event) => {
handleClose();
}}
style={{fontSize: 18}}
>
<LightbulbIcon style={{ marginRight: 5 }} /> Use Cases
</MenuItem>
</Link>
<Divider style={{ marginTop: 10, marginBottom: 10, }} />
@@ -537,7 +525,7 @@ useEffect(() => {
<Divider style={{ marginBottom: 10, }} />
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: 2.0.0-rc2
Version: 2.0.0
</Typography>
</Menu>
</span>
@@ -670,8 +658,8 @@ useEffect(() => {
};
const getRegionTag = (region_url) => {
//let regiontag = "UK";
let regiontag = "EU";
let regiontag = "UK";
//let regiontag = "EU";
if (
region_url !== undefined &&
region_url !== null &&
@@ -704,16 +692,44 @@ useEffect(() => {
const CheckOrgStates = useCallback(() => {
setOrgOptions(
userdata?.orgs?.map((org) => ({
id: org.id,
name: org.name,
image: org.image,
region_url: getRegionTag(org.region_url),
})) || []
userdata?.orgs?.map((org) => {
let skipOrg = false;
if (
org.creator_org !== undefined &&
org.creator_org !== null &&
org.creator_org.length > 0
) {
// Finds the parent org
for (let key in userdata.child_orgs) {
if (userdata.child_orgs[key].id === org.creator_org) {
skipOrg = true;
break;
}
}
if (skipOrg) {
return null; // Skip this org
}
}
return {
id: org.id,
name: org.name,
image: org.image,
region_url: getRegionTag(org.region_url),
margin_left:
org.creator_org !== undefined &&
org.creator_org !== null &&
org.creator_org.length > 0 ? 20 : 0,
};
}) || []
);
setActiveOrgName(userdata?.active_org?.name || "Select Organization");
setSelectedOrg(userdata?.active_org?.name || "Select Organization");
},[orgOptions, activeOrgName, selectedOrg]);
}, [userdata]);
useEffect(() => {
if (typeof userdata?.id === "string" && userdata?.id?.length > 0) {
@@ -735,37 +751,8 @@ useEffect(() => {
fontSize: 18
};
const modalView = (
<Dialog
open={searchBarModalOpen}
onClose={() => {
setSearchBarModalOpen(false);
}}
PaperProps={{
style: {
color: "white",
minWidth: 750,
height: 785,
borderRadius: 16,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #000000)",
boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)",
zIndex: 13000,
paddingTop: 20,
},
}}
>
<DialogContent className='dialog-content' style={{}}>
<SearchBox globalUrl={globalUrl} serverside={serverside} userdata={userdata} />
</DialogContent>
<Divider style={{overflow: "hidden"}}/>
<span style={{display:"flex", width:"100%", height:30}}>
</span>
</Dialog>
);
const getRegionFlag = (region_url) => {
var region = "gb";
var region = "UK";
const regionMapping = {
"US": "us",
"EU": "eu",
@@ -801,20 +788,8 @@ useEffect(() => {
zoom: 0.8,
height: "calc((100vh - 32px)*1.2)",
}}
onMouseLeave={() => {
if (window?.location?.pathname?.includes("/workflows/")) {
setExpandLeftNav(false);
}
}}
onMouseOver={() => {
if (window?.location?.pathname?.includes("/workflows/")) {
setExpandLeftNav(true);
}
}
}
>
{modalView}
{searchBarModalOpen ? <ModalView serverside={serverside} userdata={userdata} searchBarModalOpen={searchBarModalOpen} setSearchBarModalOpen={setSearchBarModalOpen} globalUrl={globalUrl} /> : null}
<Box
sx={{
display: "flex",
@@ -823,14 +798,47 @@ useEffect(() => {
alignItems: "center",
padding: "24px 16px 24px 27px",
}}
>
<a href="/" style={{ textDecoration: "none" }}>
<img
src={ShuffleLogo}
alt="Shuffle Logo"
style={{ width: 24, height: 24 }}
/>
</a>
onMouseOver={()=>{
if(window?.location?.pathname?.includes("/workflows/")) {
setExpandLeftNav(true)
}
}}
onMouseLeave={()=>{
if(window?.location?.pathname?.includes("/workflows/")) {
setExpandLeftNav(false)
}
}}
>
<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",
@@ -871,7 +879,7 @@ useEffect(() => {
</Button>
</Box>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", width:"100%", height: "100%", overflowY: "auto", overflowX: "hidden",transition: 'display 0.3s ease',paddingTop: 0.5 }} onMouseOver={()=>{!leftSideBarOpenByClick && setExpandLeftNav(true);}} onMouseLeave={()=>{!leftSideBarOpenByClick && setExpandLeftNav(false);setOpenAutocomplete(false);}}>
<Box sx={{ display: "flex", flexDirection: "column", width:"100%", height: "100%", overflowY: "auto", overflowX: "hidden",transition: 'display 0.3s ease',paddingTop: 0.5 }} onMouseOver={()=>{(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(true)}} onMouseLeave={()=>{(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(false);setOpenAutocomplete(false)}}>
<Box
sx={{
display: "flex",
@@ -1145,7 +1153,7 @@ useEffect(() => {
color: currentOpenTab === "apps" && currentPath.includes("/apps") ? "#FFFFFF" : "#C8C8C8",
justifyContent: "flex-start",
textTransform: "none",
backgroundColor: currentOpenTab === "apps" && expandLeftNav && currentPath.includes("/apps2") ? "#2f2f2f": "transparent",
backgroundColor: currentOpenTab === "apps" && expandLeftNav && currentPath.includes("/apps") ? "#2f2f2f": "transparent",
marginLeft: 16,
fontSize: 18
}}
@@ -1153,7 +1161,7 @@ useEffect(() => {
event.currentTarget.style.backgroundColor = "#2f2f2f";
}}
onMouseOut={(event)=>{
event.currentTarget.style.backgroundColor = currentOpenTab === "apps" && expandLeftNav && currentPath.includes("/search") ? "#2f2f2f": "transparent";
event.currentTarget.style.backgroundColor = currentOpenTab === "apps" && expandLeftNav && currentPath.includes("/apps") ? "#2f2f2f": "transparent";
}}
disableRipple={expandLeftNav ? false : true}
>
@@ -1176,7 +1184,7 @@ useEffect(() => {
sx={{
display: "flex",
flexDirection: "row",
marginTop: 1
marginTop: 0
}}
>
<span style={{ display: "inline-block", width: "100%" }}>
@@ -1206,12 +1214,12 @@ useEffect(() => {
: "transparent";
}}
>
<ShieldOutlinedIcon
<TocIcon
style={{
width: 18,
height: 18,
marginRight: expandLeftNav ? 10 : 0,
color: userdata?.support ? "inherit" : "#6F6F6F",
color: "inherit",
}}
/>
<span
@@ -1224,7 +1232,7 @@ useEffect(() => {
: "#C8C8C8"
}}
>
Discover
Content
</span>
</Button>
</Link>
@@ -1263,7 +1271,7 @@ useEffect(() => {
<Collapse in={openSecurityTab} timeout="auto" unmountOnExit>
<Box
style={{
maxHeight: openSecurityTab && expandLeftNav ? 100 : 0,
maxHeight: openSecurityTab && expandLeftNav ? 135 : 0,
overflow: "hidden",
transition: "max-height 0.3s ease, opacity 0.3s ease",
display: "flex",
@@ -1277,19 +1285,18 @@ useEffect(() => {
to={"/forms"}
style={{
...hrefStyle,
pointerEvents: userdata?.support ? "auto" : "none",
pointerEvents: "auto",
}}
>
<Button
onClick={(event) => {
if (!userdata?.support) return;
setCurrentOpenTab("detection");
localStorage.setItem("lastTabOpenByUser", "detection");
}}
sx={{
width: "100%",
height: 35,
color: userdata?.support ? "#C8C8C8" : "#6F6F6F",
color: "#C8C8C8",
justifyContent: "flex-start",
textTransform: "none",
backgroundColor:
@@ -1297,11 +1304,10 @@ useEffect(() => {
? "#2f2f2f"
: "transparent",
"&:hover": {
backgroundColor: userdata?.support ? "#2f2f2f" : "transparent",
backgroundColor: "#2f2f2f",
},
cursor: userdata?.support ? "pointer" : "not-allowed",
cursor: "pointer",
}}
disabled={userdata?.support === false}
>
<span style={{ position: "relative", left: !expandLeftNav ? 10 : 0, marginRight: 10, fontSize: 18 }}>
@@ -1313,11 +1319,9 @@ useEffect(() => {
transition: "opacity 0.3s ease",
fontSize: 18,
color:
userdata?.support && currentOpenTab === "detection" && currentPath.includes("/detection")
currentOpenTab === "detection" && currentPath.includes("/detection")
? "#F1F1F1"
: userdata?.support
? "#C8C8C8"
: "#6F6F6F",
: "#C8C8C8"
}}
>
Forms
@@ -1330,19 +1334,18 @@ useEffect(() => {
to={"/admin?tab=datastore"}
style={{
...hrefStyle,
pointerEvents: userdata?.support ? "auto" : "none",
pointerEvents: "auto",
}}
>
<Button
onClick={(event) => {
if (!userdata?.support) return;
setCurrentOpenTab("response");
localStorage.setItem("lastTabOpenByUser", "response");
}}
sx={{
width: "100%",
height: 35,
color: userdata?.support ? "#C8C8C8" : "#6F6F6F",
color: "#C8C8C8",
justifyContent: "flex-start",
textTransform: "none",
backgroundColor:
@@ -1350,9 +1353,9 @@ useEffect(() => {
? "#2f2f2f"
: "transparent",
"&:hover": {
backgroundColor: userdata?.support ? "#2f2f2f" : "transparent",
backgroundColor: "#2f2f2f",
},
cursor: userdata?.support ? "pointer" : "not-allowed",
cursor: "pointer",
}}
>
<span style={{ position: "relative", left: !expandLeftNav ? 10 : 0, marginRight: 10, fontSize: 18 }}>
@@ -1365,11 +1368,9 @@ useEffect(() => {
transition: "opacity 0.3s ease",
fontSize: 18,
color:
userdata?.support && currentOpenTab === "response" && currentPath.includes("/response")
currentOpenTab === "response" && currentPath.includes("/response")
? "#F1F1F1"
: userdata?.support
? "#C8C8C8"
: "#6F6F6F",
: "#C8C8C8"
}}
>
Datastore
@@ -1377,24 +1378,24 @@ useEffect(() => {
</Button>
</Link>
</span>
<span style={{ display: "inline-block", width: "100%" }}>
<Link
to={"/admin?tab=files"}
style={{
...hrefStyle,
pointerEvents: userdata?.support ? "auto" : "none",
pointerEvents: "auto",
}}
>
<Button
onClick={(event) => {
if (!userdata?.support) return;
setCurrentOpenTab("response");
localStorage.setItem("lastTabOpenByUser", "response");
}}
sx={{
width: "100%",
height: 35,
color: userdata?.support ? "#C8C8C8" : "#6F6F6F",
color: "#C8C8C8",
justifyContent: "flex-start",
textTransform: "none",
backgroundColor:
@@ -1402,9 +1403,9 @@ useEffect(() => {
? "#2f2f2f"
: "transparent",
"&:hover": {
backgroundColor: userdata?.support ? "#2f2f2f" : "transparent",
backgroundColor: "#2f2f2f",
},
cursor: userdata?.support ? "pointer" : "not-allowed",
cursor: "pointer",
}}
>
<span style={{ position: "relative", left: !expandLeftNav ? 10 : 0, marginRight: 10, fontSize: 18 }}>
@@ -1417,11 +1418,9 @@ useEffect(() => {
transition: "opacity 0.3s ease",
fontSize: 18,
color:
userdata?.support && currentOpenTab === "response" && currentPath.includes("/response")
currentOpenTab === "response" && currentPath.includes("/response")
? "#F1F1F1"
: userdata?.support
? "#C8C8C8"
: "#6F6F6F",
: "#C8C8C8"
}}
>
Files
@@ -1429,8 +1428,59 @@ useEffect(() => {
</Button>
</Link>
</span>
<span style={{ display: "inline-block", width: "100%" }}>
<Link
to={"/admin?tab=locations"}
style={{
...hrefStyle,
pointerEvents: "auto",
}}
>
<Button
onClick={(event) => {
setCurrentOpenTab("response");
localStorage.setItem("lastTabOpenByUser", "response");
}}
sx={{
width: "100%",
height: 35,
color: "#C8C8C8",
justifyContent: "flex-start",
textTransform: "none",
backgroundColor:
currentOpenTab === "response" && currentPath.includes("/response")
? "#2f2f2f"
: "transparent",
"&:hover": {
backgroundColor: "#2f2f2f",
},
cursor: "pointer",
}}
>
<span style={{ position: "relative", left: !expandLeftNav ? 10 : 0, marginRight: 10, fontSize: 18 }}>
</span>
<span
style={{
display: expandLeftNav ? "inline" : "none",
opacity: expandLeftNav ? 1 : 0,
transition: "opacity 0.3s ease",
fontSize: 18,
color:
currentOpenTab === "response" && currentPath.includes("/response")
? "#F1F1F1"
: "#C8C8C8"
}}
>
Hybrid Locations
</span>
</Button>
</Link>
</span>
</Box>
</Collapse>
<Link to="/docs" style={hrefStyle}>
<Button
onClick={(event) => {
@@ -1465,6 +1515,38 @@ useEffect(() => {
</span>
</Button>
</Link>
<Link to={isCloud ? "/admin?admin_tab=billingstats" : "/admin?admin_tab=locations"} style={hrefStyle}>
<Button
onClick={(event) => {
setCurrentOpenTab("admin");
localStorage.setItem("lastTabOpenByUser", "admin");
}}
style={{
...ButtonStyle,
marginTop: 8,
marginTop: 8,
backgroundColor: currentOpenTab === "docs" && currentPath.includes("/admin") ? "#2f2f2f": "transparent",
}}
onMouseOver={(event)=>{
event.currentTarget.style.backgroundColor = "#2f2f2f";
}}
onMouseOut={(event)=>{
event.currentTarget.style.backgroundColor = currentOpenTab === "docs" && currentPath.includes("/admin") ? "#2f2f2f": "transparent";
}}
>
<BusinessIcon style={{ width: 16, height: 16, marginRight: expandLeftNav ? 10 : 0, color: "rgba(255,255,255,0.5)", }} />
<span
style={{
display: expandLeftNav ? "inline" : "none",
color: currentOpenTab === "admin" && currentPath.includes("/admin") ? "#F1F1F1" : "#C8C8C8",
}}
>
Admin
</span>
</Button>
</Link>
</Box>
{recentworkflows?.length > 0 ?
@@ -1550,6 +1632,7 @@ useEffect(() => {
padding: option.id === "add_suborg" ? "0" : "12px 16px",
marginTop: index !== 0 ? 8 : 0,
borderRadius: 6,
marginLeft: option.margin_left ? option.margin_left : 0,
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = "#444444";
@@ -1619,12 +1702,15 @@ useEffect(() => {
setAutocompleteValue(newInputValue);
}}
filterOptions={(options, params) => {
const normalize = (str) => str.toLowerCase().replace(/[\s-]+/g, "");
const input = normalize(params.inputValue);
return options.filter((option) =>
option.name
.toLowerCase()
.includes(params.inputValue.toLowerCase())
normalize(option.name).includes(input) ||
normalize(option.region_url).includes(input)
);
}}
value={userOrgs}
renderInput={(params) => (
<Box
@@ -1812,3 +1898,37 @@ useEffect(() => {
};
export default LeftSideBar;
const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata}) => {
return (
(
<Dialog
open={searchBarModalOpen}
onClose={() => {
setSearchBarModalOpen(false);
}}
PaperProps={{
style: {
color: "white",
minWidth: 750,
height: 785,
borderRadius: 16,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #000000)",
boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)",
zIndex: 13000,
paddingTop: 20,
},
}}
>
<DialogContent className='dialog-content' style={{}}>
<SearchBox globalUrl={globalUrl} serverside={serverside} userdata={userdata} />
</DialogContent>
<Divider style={{overflow: "hidden"}}/>
<span style={{display:"flex", width:"100%", height:30}}>
</span>
</Dialog>
)
)
});
+3 -3
View File
@@ -869,8 +869,8 @@ const LicencePopup = (props) => {
:
shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9"
const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success`
const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure`
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
console.log("Priceitem: ", priceItem, shuffleVariant)
var checkoutObject = {
@@ -888,7 +888,7 @@ const LicencePopup = (props) => {
}
if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) {
window.open("https://shuffler.io/admin?admin_tab=billing&payment=stripe_error", "_self")
window.open("https://shuffler.io/admin?admin_tab=billingstats&payment=stripe_error", "_self")
}
stripe.redirectToCheckout(checkoutObject)
+2 -2
View File
@@ -465,7 +465,7 @@ const Header = (props) => {
</Link>
<Divider style={{ marginTop: 10, marginBottom: 10, }} />
<Link to="/admin?admin_tab=priorities" style={hrefStyle}>
<Link to="/admin?admin_tab=notifications" style={hrefStyle}>
<MenuItem
onClick={(event) => {
handleClose();
@@ -1107,7 +1107,7 @@ const Header = (props) => {
);
})}
<Divider />
<Link to="/admin?tab=suborgs" style={hrefStyle}>
<Link to="/admin?tab=tenants" style={hrefStyle}>
<MenuItem
key={"add suborgs"}
style={{
+1 -1
View File
@@ -431,7 +431,7 @@ const AuthenticationOauth2 = (props) => {
const authentication_url = authenticationType.token_uri;
const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`;
const workflowId = workflow !== undefined ? workflow.id : "";
const workflowId = workflow !== undefined ? workflow.id : ""
var state = `workflow_id%3D${workflowId}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}`;
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+221
View File
@@ -0,0 +1,221 @@
import React, { useEffect, useState, useCallback } from 'react';
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"
import { ToastContainer, toast } from "react-toastify";
import { Button, Tooltip } from '@mui/material';
const OrganizationTab = (props) => {
const location = useLocation();
const navigate = useNavigate();
const {
userdata,
globalUrl,
serverside,
isCloud,
checkLogin,
notifications,
setNotifications,
stripeKey, setSelectedOrganization,
selectedStatus, setSelectedStatus,
selectedOrganization, handleGetOrg,
handleStatusChange, handleEditOrg,
isLoaded,
removeCookie
} = props;
const [selectedTab, setSelectedTab] = useState('org_config');
const [organizationFeatures, setOrganizationFeatures] = useState({});
const [billingInfo, setBillingInfo] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const [curIndex, setCurIndex] = React.useState(0);
const [unreadNotifications, setUnreadNotifications] = React.useState(
notifications?.filter((notification) => notification.read === false)?.length
);
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams.get('admin_tab');
if (tabName) {
const decodedTabName = decodeURIComponent(tabName);
setSelectedTab(decodedTabName);
if (decodedTabName === 'org_config') {
setCurIndex(0);
} else if(decodedTabName === 'sso'){
setCurIndex(1)
}else if (decodedTabName === 'notifications' || decodedTabName === 'priorities') {
setCurIndex(2);
} else if (decodedTabName === 'billingstats' || decodedTabName === 'billing') {
setCurIndex(3);
} else if (decodedTabName === 'branding(beta)') {
setCurIndex(4);
}
// else if (decodedTabName === 'analytics') {
// setCurIndex(5);
// }
}
}, [location.search]);
const handleTabClick = (tabName) => {
const formattedTabName = tabName.toLowerCase().replace(/[\s&]+/g, '');
const encodedTabName = encodeURIComponent(formattedTabName);
setSelectedTab(formattedTabName);
document.title = `Shuffle - admin - ${formattedTabName}`;
navigate(`?admin_tab=${encodedTabName}`);
};
const handleNotifications = useCallback(() => {
const unreadCount = notifications?.filter((notification) => notification.read === false).length;
setUnreadNotifications(unreadCount);
},[unreadNotifications,notifications]);
useEffect(() => {
if ((unreadNotifications !== notifications?.filter((notification) => notification.read === false).length) !== unreadNotifications) {
handleNotifications();
}
}, [notifications]);
const renderContent = () => {
switch (selectedTab) {
case 'org_config':
return <EditOrgTab isCloud={isCloud} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} />;
case 'sso':
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
case `notifications`:
case `priorities`:
return (
<Priorities
isCloud={isCloud}
userdata={userdata}
globalUrl={globalUrl}
checkLogin={checkLogin}
notifications={notifications}
setNotifications={setNotifications}
clickedFromOrgTab={true}
serverside={serverside}
isLoaded={isLoaded}
selectedOrganization={selectedOrganization}
handleEditOrg={handleEditOrg}
/>
);
case 'billingstats' :
case 'billing' :
return (
<Billing
isCloud={true}
userdata={userdata}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
billingInfo={billingInfo}
stripeKey={stripeKey}
handleGetOrg={handleGetOrg}
clickedFromOrgTab={true}
handleEditOrg={handleEditOrg}
removeCookie={removeCookie}
isLoaded={isLoaded}
/>
);
case 'branding(beta)':
return <Branding
isCloud={isCloud}
userdata={userdata}
globalUrl={globalUrl}
handleGetOrg={handleGetOrg}
selectedOrganization={selectedOrganization}
clickedFromOrgTab={true}
setSelectedOrganization={setSelectedOrganization}
/>;
// case 'analytics':
// return <AnalyticsTab isCloud={isCloud} userdata={userdata} globalUrl={globalUrl} />;
default:
return <EditOrgTab isCloud={isCloud} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} />;
}
};
return (
<div style={{ height: "100%", width: "100%", color: '#FFFFFF', backgroundColor: '#212121', borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", boxSizing: 'border-box' }}>
<div style={{ display: 'flex', justifyContent: 'space-around', width: "100%", borderBottom: '1px solid #494949' ,boxSizing: 'border-box' }}>
{['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding (Beta)'].map((tabName, index) => (
<Tooltip
key={index}
title={
((tabName === "sso")) && !(userdata?.support || userdata?.active_org?.role === "admin")
? "Your role is not admin. Please ask the admin to change your role."
: ""
}
placement="right"
>
<div style={{ pointerEvents: 'auto', width: '100%',}}>
<Button
key={tabName}
onClick={() => {
setCurIndex(index);
handleTabClick(index === 0 ? "org_config" : tabName.toLowerCase().replace(/[\s&]+/g, ''));
}}
variant="text"
sx={{
"&.MuiButton-root": {
padding: '28px 0',
borderBottom: index === curIndex ? '2px solid #FF8444' : 'none',
cursor: 'pointer',
fontWeight: index === curIndex ? 'bold' : 'normal',
color: index === curIndex ? "#FF8444" : "#FFFFFF",
textTransform: 'none',
fontSize: 16,
width: "100%",
height: "100%",
borderRadius: 0,
},
"&: hover": {
backgroundColor: "#323232"
},
"&.Mui-disabled": {
color: "#6F6F6F",
},
}}
disabled={
((tabName === "sso")) && !(userdata?.support || userdata?.active_org?.role === "admin")
}
>
{index === 2 && unreadNotifications > 0 ? (
<div style={{ position: 'relative' }}>
<div style={{
position: 'absolute',
top: -12,
right: -15,
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: '#FF8444',
color: '#FFFFFF',
fontSize: 12,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
{unreadNotifications}
</div>
{tabName}
</div>
) : (
<>{index === 1 ? "SSO" : tabName}</>
)}
</Button>
</div>
</Tooltip>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'center', width: "100%", height: "100%", boxSizing:'border-box'}}>
{renderContent()}
</div>
</div>
);
};
export default OrganizationTab;
File diff suppressed because it is too large Load Diff
+381 -4
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext, memo } from "react";
import { toast } from "react-toastify";
import theme from "../theme.jsx";
import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid";
import {
Paper,
Tooltip,
@@ -13,8 +14,17 @@ import {
Card,
Chip,
Switch,
Skeleton,
Autocomplete,
TextField,
MenuItem,
IconButton,
} from "@mui/material";
import {
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
import { makeStyles } from "@mui/styles";
import { Context } from "../context/ContextApi.jsx";
import { useNavigate, Link } from "react-router-dom";
@@ -22,8 +32,15 @@ import Priority from "../components/Priority.jsx";
import { constrainMatrix } from "reaviz";
//import { useAlert
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const Priorities = memo((props) => {
const { globalUrl, userdata,clickedFromOrgTab, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
const { globalUrl, userdata,clickedFromOrgTab,selectedOrganization, handleEditOrg, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
const [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false);
@@ -31,8 +48,22 @@ const Priorities = memo((props) => {
const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT");
const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT");
const [highlightKMS, setHighlightKMS] = React.useState(false)
const [workflows, setWorkflows] = React.useState([])
const [openNotification, setOpenNotification] = React.useState(false);
const [workflow, setWorkflow] = React.useState({})
const [notificationWorkflow, setNotificationWorkflow] = React.useState(
selectedOrganization.defaults === undefined
? ""
: selectedOrganization.defaults.notification_workflow === undefined ||
selectedOrganization.defaults.notification_workflow.length === 0
? ""
: selectedOrganization.defaults.notification_workflow
);
let navigate = useNavigate();
const classes = useStyles();
useEffect(() => {
getFramework()
@@ -60,6 +91,20 @@ const Priorities = memo((props) => {
}
}, [])
useEffect(() => {
if (selectedOrganization === undefined || selectedOrganization === null || selectedOrganization?.id === undefined || selectedOrganization?.id === null || selectedOrganization?.id.length === 0) {
return
}
if(workflows?.length === 0) {
getAvailableWorkflows()
}
if (notificationWorkflow !== selectedOrganization?.defaults?.notification_workflow) {
setNotificationWorkflow(selectedOrganization?.defaults?.notification_workflow)
}
}, [selectedOrganization])
if (userdata === undefined || userdata === null) {
return
}
@@ -220,11 +265,341 @@ const Priorities = memo((props) => {
const imagesize = 22
const boxColor = "#86c142"
const getAvailableWorkflows = () => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson !== undefined) {
// Add parent notification workflow if it's a child org
// selectedOrganization,
if (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org.length > 0) {
// Add to start of the list
responseJson.unshift({
"name": "Parent-Org's Notification Workflow",
"id": "parent",
})
}
setWorkflows(responseJson)
if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) {
const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow)
if (workflow !== undefined && workflow !== null) {
setWorkflow(workflow)
}
}
}
})
.catch((error) => {
console.log("Error getting workflows: " + error);
})
}
const handleWorkflowSelectionUpdate = (e, isUserinput) => {
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
console.log("Returning as there's no id")
return null
}
setOpenNotification(false)
setWorkflow(e.target.value)
setNotificationWorkflow(e.target.value.id)
handleEditOrg(
selectedOrganization?.name,
selectedOrganization.description,
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: e.target.value.id,
documentation_reference: selectedOrganization?.defaults?.documentation_reference,
workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
newsletter: selectedOrganization?.defaults?.newsletter,
weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
},
{
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,
}
)
}
return (
<div style={{width: "100%", height: "100%", boxSizing: 'border-box', transition: 'width 0.3s ease', padding: clickedFromOrgTab ? "27px 10px 19px 27px":null, height: clickedFromOrgTab ? "auto":null, minHeight: 843, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{maxWidth: "calc(100% - 20px)"}}>
<Typography style={{ fontSize: 24, fontWeight: 'bold', display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications ({
<Typography variant="h5" style={{ color: "rgba(241, 241, 241, 1)", fontSize: 24, fontWeight: 600, textAlign: "left" }}>
Notification Workflow
</Typography>
<Typography style={{ color: "rgba(158, 158, 158, 1)", fontSize: 16, fontWeight: 400, marginTop: 5, }}>
The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org notifications into the parent org notification by choosing it in the list.</b>
</Typography>
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", }}>
{workflows !== undefined && workflows !== null && workflows.length > 0 ?
<Autocomplete
id="notification_workflow_search"
autoHighlight
open={openNotification}
onOpen={() => {
setOpenNotification(true);
}}
onClose={() => {
setOpenNotification(false);
}}
freeSolo
//autoSelect
value={workflows?.find(w => w.id === notificationWorkflow) || null}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: "#212121",
color: "white",
},
}}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.name === undefined ||
option.name === null
) {
return "No Workflow Selected";
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={workflows}
fullWidth
style={{
backgroundColor: "#212121",
borderRadius: theme.palette?.borderRadius,
height: 35,
marginBottom: 40,
}}
onChange={(event, newValue) => {
console.log("Found value: ", newValue)
var parsedinput = { target: { value: newValue } }
// For variables
if (typeof newValue === 'string' && newValue.startsWith("$")) {
parsedinput = {
target: {
value: {
"name": newValue,
"id": newValue,
"actions": [],
"triggers": [],
}
}
}
}
handleWorkflowSelectionUpdate(parsedinput)
}}
renderOption={(props, data, state) => {
if (data.id === workflow.id) {
data = workflow;
}
return (
<Tooltip arrow placement="right" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose {data.name}
</Typography>
</span>
} placement="bottom">
<MenuItem
{...props}
style={{
// backgroundColor: theme.palette.inputColor,
color: data.id === workflow.id ? "red" : "white",
borderBottom: data.id === "parent" ? "2px solid rgba(255,255,255,0.5)" : null
}}
value={data}
onClick={(e) => {
props.onMouseDown?.(null);
var parsedinput = { target: { value: data } }
handleWorkflowSelectionUpdate(parsedinput)
}}
>
{data.name}
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
style={{
backgroundColor: "rgba(33, 33, 33, 1)",
borderRadius: 4,
height: 35,
fontSize: 16,
marginTop: "16px"
}}
InputProps={{
...params.InputProps,
style: {
height: 35,
display: "flex",
alignItems: "center",
padding: "0px 8px",
fontSize: 16,
borderRadius: 4,
},
inputProps: {
...params.inputProps,
style: {
height: "100%",
boxSizing: "border-box",
}
}
}}
// label="Find a notification workflow"
variant="outlined"
placeholder="Select a notification workflow"
/>
);
}}
/>
:
<TextField
required
InputProps={{
style: {
height: 35,
display: "flex",
alignItems: "center",
padding: "0px 8px",
fontSize: 16,
borderRadius: 4,
},
inputProps: {
style: {
height: "100%",
boxSizing: "border-box",
}
}
}}
style={{
backgroundColor: "rgba(33, 33, 33, 1)",
borderRadius: 4,
height: 35,
fontSize: 16,
marginBottom: 30
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="ID of the workflow to receive notifications"
value={notificationWorkflow}
onChange={(e) => {
setNotificationWorkflow(e.target.value);
}}
/>
}
{/* <div style={{ minWidth: 150, maxWidth: 150, marginTop: 5, marginLeft: 10, }}>
{orgSaveButton}
</div> */}
</div>
{notificationWorkflow === undefined || notificationWorkflow === null || notificationWorkflow.length === 0 ? null :
<div>
<Button variant="outlined" color="secondary" style={{marginTop: 5, textTransform: "none", }} onClick={() => {
if (notificationWorkflow === "parent") {
toast.error("Can't send test notifications to the parent org's notification workflow.")
return
}
fetch(`${globalUrl}/api/v1/workflows/${notificationWorkflow}/execute`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
credentials: "include",
body: JSON.stringify({
"title": "Test Notification",
"description": "This is a test notification to check if the notification workflow is working correctly.",
"org_id": selectedOrganization.id,
"id": uuidv4(),
"reference_url": "/admin?type=test&admin_tab=notifications",
"created_at": Math.floor(new Date().getTime() / 1000),
"updated_at": Math.floor(new Date().getTime() / 1000),
})
})
.then((response) => {
if (response.status === 200) {
toast.success("Test notification sent successfully.")
} else {
toast.error("Failed to send test notification. Please contact support if this persists")
}
}).catch((error) => {
toast.error("Failed to send test notification (2). Please contact support if this persists")
})
}}>
Send test notification
</Button>
<IconButton
style={{marginLeft: 10, }}
onClick={() => {
if (notificationWorkflow === "parent") {
toast.error("Can't open parent org's notification workflow from here.")
return
}
window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank")
}}
>
<OpenInNewIcon color="primary" />
</IconButton>
</div>
}
<Typography style={{marginTop: 50, fontSize: 24, fontWeight: 'bold', display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications ({
notifications?.filter((notification) => showRead === true || notification.read === false).length
})</Typography>
@@ -261,10 +636,12 @@ const Priorities = memo((props) => {
</Button>
) : null}
</div>
<NotificationComponent notifications={notifications} showRead={showRead} selectedExecutionId={selectedExecutionId} selectedWorkflow={selectedWorkflow} highlightKMS={highlightKMS} userdata={userdata} imagesize={imagesize} boxColor={boxColor} clickedFromOrgTab={clickedFromOrgTab} notificationWidth={notificationWidth} dismissNotification={dismissNotification}/>
{clickedFromOrgTab? null : <Divider style={{marginTop: 50, marginBottom: 50, }} />}
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ? 30 :null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ? 60 : null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
<span style={{ fontSize: 16, color: clickedFromOrgTab ?"#9E9E9E":null,marginLeft: clickedFromOrgTab ?null:25, }}>
Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. <br/>These range from simple configurations in Shuffle to Usecases you may have missed.&nbsp;
<a
+162 -94
View File
@@ -15,6 +15,7 @@ import {
Tooltip,
Typography,
IconButton,
Switch,
} from '@mui/material';
import { toast } from "react-toastify"
@@ -36,6 +37,9 @@ import {
Insights as InsightsIcon,
Replay as ReplayIcon,
EditNote as EditNoteIcon,
AccountTree as AccountTreeIcon,
Cached as CachedIcon,
FilterAltOff as FilterAltOffIcon
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
@@ -73,6 +77,8 @@ const RuntimeDebugger = (props) => {
const [rowsPerPage, setRowsPerPage] = useState(10)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [suborgWorkflowRuns, setSuborgWorkflowRuns] = useState(false)
const [openWorkflowMenu, setOpenWorkflowMenu] = useState(false)
const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",}
])
@@ -162,7 +168,7 @@ const RuntimeDebugger = (props) => {
}, maxworkflows*300)
}
const submitSearch = (workflowId, status, startTime, endTime, cursor, limit) => {
const submitSearch = (workflowId, status, startTime, endTime, cursor, limit, suborg_runs) => {
handleWorkflowUsageCount(workflows)
//setResultRows([])
setSearchLoading(true)
@@ -175,6 +181,7 @@ const RuntimeDebugger = (props) => {
start_time: startTime,
end_time: endTime,
ignore_org: ignoreOrg,
suborg_runs: suborg_runs,
}
fetch(`${globalUrl}/api/v1/workflows/search`, {
@@ -344,10 +351,28 @@ const RuntimeDebugger = (props) => {
source = "manual"
}
var imageSource = "";
if (params?.row?.org?.id?.length > 0) {
if (params?.row?.org?.image?.length > 0){
imageSource = params?.row.org?.image
}else {
imageSource = "/images/no_image.png"
}
}else {
if (userdata.active_org.image?.length > 0){
imageSource = userdata?.active_org?.image
}else {
imageSource = "/images/no_image.png"
}
}
return (
<span style={{}} onClick={() => {
//setStatus(params.row.status)
}}>
{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">
{foundSource}
</Tooltip>
@@ -372,7 +397,7 @@ const RuntimeDebugger = (props) => {
headerName: 'Workflow Name',
width: 250,
renderCell: (params) => (
<span style={{cursor: "pointer", }} onClick={() => {
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", cursor: "pointer", }} onClick={() => {
setWorkflowId(params.row.workflow.id)
for (let key in workflows) {
@@ -382,8 +407,18 @@ const RuntimeDebugger = (props) => {
}
}
}}>
{params.row.workflow.name}
</span>
<span>{params.row.workflow.name}</span>
{params?.row?.org?.id?.length > 0 &&
<Tooltip title={(
<div style={{display: "flex", }}>
<img src={params.row.org.image || "/images/no_image.png"} alt={params.row.org.name} style={{height: 24, width: 24, borderRadius: 12, }} />
<Typography variant="body2" style={{marginLeft: 5, }}>{params.row.org.name}</Typography>
</div>
)} placement="top" arrow>
<AccountTreeIcon style={{color: "#B0B0B0", height: "24px", width: "24px", marginLeft: 10}} />
</Tooltip>
}
</div>
),
},
@@ -630,7 +665,7 @@ const RuntimeDebugger = (props) => {
return
}
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
}, [workflowId, status, startTime, endTime])
const textfieldStyle = {
@@ -679,8 +714,9 @@ const RuntimeDebugger = (props) => {
setWorkflow(e.target.value)
setWorkflowId(e.target.value.id)
setSuborgWorkflowRuns(false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, rowsPerPage)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, rowsPerPage, false)
}
const executeWorkflow = (execution) => {
@@ -773,84 +809,17 @@ const RuntimeDebugger = (props) => {
return (
<div style={{minWidth: 1150, maxWidth: 1150, margin: "auto", }}>
<div style={{display: "flex", }}>
<div style={{display: "flex", paddingTop: 50, }}>
<div style={{display: 'flex', flexDirection: 'column'}}>
<h1 style={{flex: 3, }}>Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}</h1>
<div style={{position: 'relative', right: 10, marginBottom: 10}}>
<TextField
fullWidth
value={searchQuery}
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 20,
marginLeft: 10,
marginRight: 12,
width: 693,
height: 55,
borderRadius: 8,
fontSize: 16,
marginBottom: 15,
}}
InputProps={{
style: {
color: "white",
fontSize: "1em",
height: 55,
width: 693,
borderRadius: 8,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ marginLeft: 5}} />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{searchQuery.length > 0 && (
<ClearIcon
style={{
color: "white",
cursor: "pointer",
marginRight: 10
}}
onClick={() => setSearchQuery('')}
/>
)}
<button
type="button"
style={{
backgroundImage:
"linear-gradient(to right, rgb(248, 106, 62), rgb(243, 64, 121))",
color: "white",
border: "none",
padding: "10px 20px",
width: 100,
height: 35,
borderRadius: 17.5,
cursor: "pointer",
}}
>
Search
</button>
</InputAdornment>
),
}}
onChange={(e)=>{handleQueryChange(e)}}
color="primary"
placeholder="Filter by Workflow Name, Status, Execution Argument, Results.."
id="shuffle_search_field"
/>
</div>
</div>
{selectedWorkflowExecutions.length > 0 ?
<div style={{display: "flex", width: "100%", }}>
<h1 style={{flex: 3, whiteSpace: "nowrap" }}>Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}</h1>
{selectedWorkflowExecutions.length > 0 ?
<ButtonGroup>
<Tooltip title="Reruns ALL selected workflows. This will make a new execution for them, and not continue the existing.">
<Button
variant="outlined"
color="secondary"
style={{maxHeight: 40, marginTop: 25, }}
style={{maxHeight: 40, marginTop: 25, marginLeft: 20, }}
onClick={() => {
for (var i = 0; i < selectedWorkflowExecutions.length; i++) {
@@ -888,7 +857,7 @@ const RuntimeDebugger = (props) => {
} else {
toast("Aborted "+aborted+" workflows.")
// Research
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
setSelectedWorkflowExecutions([])
}
@@ -906,7 +875,7 @@ const RuntimeDebugger = (props) => {
<Button
variant={ignoreOrg ? "contained" : "outlined"}
color="secondary"
style={{marginLeft: 100, maxHeight: 40, marginTop: 25, }}
style={{marginLeft: 20, maxHeight: 40, marginTop: 25, }}
onClick={() => {
setIgnoreOrg(!ignoreOrg)
}}
@@ -914,10 +883,83 @@ const RuntimeDebugger = (props) => {
{ignoreOrg ? "Ignoring Org" : "Ignore Org (Support Only)"}
</Button>
: null}
</div>
<div style={{display: "flex", justifyContent: "space-between", width: "100%"}}>
<div style={{position: 'relative', right: 10, marginBottom: 10}}>
<TextField
fullWidth
value={searchQuery}
style={{
backgroundColor: "#1a1a1a",
marginTop: 20,
marginLeft: 10,
marginRight: 12,
width: 693,
height: 51,
borderRadius: 4,
fontSize: 16,
}}
InputProps={{
style: {
backgroundColor: "#1a1a1a",
fontSize: "1em",
height: 51,
width: 693,
borderRadius: 4,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ marginLeft: 5}} />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{searchQuery.length > 0 && (
<ClearIcon
style={{
color: "white",
cursor: "pointer",
marginRight: 10
}}
onClick={() => setSearchQuery('')}
/>
)}
</InputAdornment>
),
}}
onChange={(e)=>{handleQueryChange(e)}}
color="primary"
placeholder="Filter by Workflow Name, Status, Execution Argument, Results"
id="shuffle_search_field"
/>
</div>
{userdata?.active_org?.creator_org?.length === 0 ? (
<div style={{display: "flex", margin: 'auto',marginTop: 20,justifyContent: 'center', alignItems: 'center', }}>
<Switch
checked={suborgWorkflowRuns}
onChange={() => {
setSuborgWorkflowRuns(!suborgWorkflowRuns);
//set selected workflow to all workflows when switching between suborg and all workflows
setWorkflowId("")
setWorkflow({"id": "", "name": "All Workflows"})
setStatus("")
setStartTime("")
setEndTime("")
setSearchQuery("")
submitSearch("", "", "", "", rowCursor, rowsPerPage, !suborgWorkflowRuns)}
}
color="secondary"
/>
<Typography variant="body2" style={{color: "white", }}>Show workflow runs from suborgs</Typography>
</div>
) : null}
</div>
</div>
</div>
<form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
}} style={{display: "flex", }}>
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
}} style={{display: "flex", justifyContent: "center", alignItems: "center", }}>
<FormControl fullWidth style={{marginTop: 5, }}>
<InputLabel id="status-label">Status</InputLabel>
<Select
@@ -951,10 +993,13 @@ const RuntimeDebugger = (props) => {
<Autocomplete
id="workflow_search"
value={workflow}
open={openWorkflowMenu}
onOpen={()=>{setOpenWorkflowMenu(true)}}
onClose={() => {setOpenWorkflowMenu(false)}}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
backgroundColor: "#1a1a1a",
color: "white",
},
}}
@@ -973,11 +1018,11 @@ const RuntimeDebugger = (props) => {
options={workflows}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
backgroundColor: "#1a1a1a",
height: 50,
borderRadius: theme.palette?.borderRadius,
marginTop: 5,
borderRadius: 4,
marginLeft: 5,
marginBottom: 3,
}}
onChange={(event, newValue) => {
console.log("Found value: ", newValue)
@@ -1018,12 +1063,13 @@ const RuntimeDebugger = (props) => {
}>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
backgroundColor: "#1a1a1a",
color: data.id === workflow.id ? "red" : "white",
}}
value={data}
onClick={(e) => {
var parsedinput = { target: { value: data } }
setOpenWorkflowMenu(false)
handleWorkflowSelectionUpdate(parsedinput)
}}
>
@@ -1036,8 +1082,8 @@ const RuntimeDebugger = (props) => {
return (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
backgroundColor: "#1a1a1a",
borderRadius: 4,
}}
{...params}
label="Workflow"
@@ -1050,8 +1096,9 @@ const RuntimeDebugger = (props) => {
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minHeight: 50,
maxHeight: 50,
minWidth: 240,
maxWidth: 240,
}}
@@ -1064,10 +1111,11 @@ const RuntimeDebugger = (props) => {
/>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
minHeight: 50,
maxHeight: 50,
}}
ampm={false}
label="Search until"
@@ -1078,14 +1126,34 @@ const RuntimeDebugger = (props) => {
/>
</LocalizationProvider>
<Tooltip title="Clear all filters and search parameters">
<Button
style={{ marginLeft: 10, minHeight: 60, marginTop: 10, backgroundColor: "#1a1a1a", border: "1px solid #424242", boxShadow: 'none', borderRadius: 4, width: 81, height: 51, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => {
setWorkflowId("")
setWorkflow({"id": "", "name": "All Workflows"})
setStatus("")
setStartTime("")
setEndTime("")
setSearchQuery("")
setSuborgWorkflowRuns(false)
submitSearch("", "", "", "", rowCursor, rowsPerPage, false)
}}
>
<FilterAltOffIcon />
</Button>
</Tooltip>
<Button
variant="outlined"
color="primary"
onClick={() => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
}}
disabled={searchLoading}
style={{height: 50, minWidth: 100, marginTop: 15, marginLeft: 10, }}
style={{height: 50, minWidth: 100, marginTop: 15, }}
>
{searchLoading ? <CircularProgress size={30} /> : "Search"}
</Button>
@@ -1100,7 +1168,7 @@ const RuntimeDebugger = (props) => {
disableSelectionOnClick
onPageSizeChange={(newPageSize) => {
setRowsPerPage(newPageSize)
submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize)
submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize, suborgWorkflowRuns)
}}
// event for when clicking next page
// Hide page changer
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -109,6 +109,12 @@ const SearchData = props => {
}
}, [searchOpen]);
useEffect(() => {
if (currentRefinement !== inputValue) {
refine(inputValue);
}
}, [currentRefinement]);
return (
<form id="search_form" noValidate type="searchbox" action="" role="search" onClick={() => {
@@ -387,7 +393,7 @@ const SearchData = props => {
if (responseJson.success === false) {
toast(`Failed to ${type} the app for your organization. Please try again or contact support@shuffler.io for more info`)
} else {
toast(`App successfully ${type}d. Please refresh the page to use it.`)
toast(`App successfully ${type}d. It may now be used in your workflows.`)
}
})
.catch(error => {
@@ -495,7 +501,7 @@ const SearchData = props => {
})
}
var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `https://shuffler.io/apps/${hit.objectID}`
var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `/apps/${hit.objectID}`
parsedUrl += `?queryID=${hit.__queryID}`
return (
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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} />
@@ -26,6 +26,7 @@ import {
Close as CloseIcon,
East as EastIcon,
Interests as InterestsIcon,
OpenInNew as OpenInNewIcon,
} from '@mui/icons-material';
import {
@@ -35,7 +36,8 @@ import {
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx";
//import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup2.jsx";
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup2.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx";
import FixWorkflowValidationErrors from "../components/FixWorkflowValidationErrors.jsx";
@@ -47,11 +49,14 @@ const WorkflowTemplatePopup = (props) => {
isModalOpenDefault,
setIsClicked,
inputWorkflowId,
inputWorkflow,
onClose,
} = props;
const [isActive, setIsActive] = useState(workflowBuilt === true);
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);
@@ -65,7 +70,7 @@ const WorkflowTemplatePopup = (props) => {
const [showTryitOut, setShowTryitout] = React.useState(showTryit === true ? true : false)
const [loadingWorkflow, setLoadingWorkflow] = React.useState(false)
const [workflow, setWorkflow] = useState({});
const [workflow, setWorkflow] = useState(inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "" ? inputWorkflow : {})
const [_, setUpdate] = useState(0)
const fetchWorkflow = (id) => {
@@ -169,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 = {
@@ -455,7 +466,7 @@ const WorkflowTemplatePopup = (props) => {
//console.log("Error in workflow template: ", responseJson.error);
setRequestSent(false)
const defaultMessage = "Error: Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io if you want manual help building this usecase until the AI system is handled."
const defaultMessage = "Error: Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io if you want manual help building this usecase."
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason !== "") {
setErrorMessage(defaultMessage + "\n\n" + responseJson.reason)
} else {
@@ -512,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
@@ -524,19 +547,13 @@ const WorkflowTemplatePopup = (props) => {
<Drawer
anchor={"right"}
open={modalOpen}
onClose={() => {
setModalOpen(false);
if (setIsClicked !== undefined) {
setIsClicked(false)
}
}}
onClose={handleClose}
PaperProps={{
style: {
backgroundColor: "black",
color: "white",
minWidth: isHomePage ? null : isMobile ? 300 : 850,
maxWidth: isHomePage ? null : isMobile ? 300 : 850,
minWidth: isHomePage ? null : isMobile ? 300 : 750,
maxWidth: isHomePage ? null : isMobile ? 300 : 750,
paddingTop: isMobile ? null : 75,
itemAlign: "center",
},
@@ -546,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>
@@ -564,7 +579,7 @@ const WorkflowTemplatePopup = (props) => {
{title === undefined || title === null || title === "" ? null :
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 25, }}>
Selected Workflow:
Selected Usecase:
</Typography>
<div style={{marginBottom: 0, }} id="workflow-template">
<WorkflowTemplatePopup2
@@ -575,9 +590,10 @@ const WorkflowTemplatePopup = (props) => {
dstapp={dstapp}
title={title}
description={description}
visualOnly={true}
visualOnly={true}
workflowBuilt={workflowBuilt}
inputWorkflow={workflow}
shownColor={shownColor}
/>
@@ -585,7 +601,7 @@ const WorkflowTemplatePopup = (props) => {
</span>
}
<div style={{marginTop: 15, }}>
<div style={{marginTop: 0, }}>
{/* Fix the timeline when errors are fixed.. how? */}
<WorkflowValidationTimeline
workflow={workflow}
@@ -601,21 +617,23 @@ const WorkflowTemplatePopup = (props) => {
</div>
{workflowLoading === true ?
<div style={{marginTop: 75, textAlign: "center", }}>
<Typography variant="h4"> Generating the Workflow...
<div style={{marginTop: 60, textAlign: "center", }}>
<Typography variant="h4"> Generating Workflows...
</Typography>
<CircularProgress style={{marginLeft: 0, marginTop: 25, }}/>
</div>
:
<div>
{usecaseDetails === undefined ? null :
<Typography variant="h6" style={{marginTop: 75, }}>
{usecaseDetails?.description}
{usecaseDetails === undefined || usecaseDetails === null || workflow.id !== undefined ? null :
<Typography variant="body1" style={{marginTop: 60, }} color="textSecondary">
{usecaseDetails?.description}
</Typography>
}
<Typography variant="h6" style={{marginTop: 75, }}>
{errorMessage !== "" ? errorMessage : ""}
</Typography>
{errorMessage !== "" ?
<Typography variant="h6" style={{marginTop: 75, }}>
{errorMessage !== "" ? errorMessage : ""}
</Typography>
: null}
{showLoginButton ?
<Link to="/register?message=Please login to create workflows&view=usecases"
style={{
@@ -643,6 +661,7 @@ const WorkflowTemplatePopup = (props) => {
variant="outlined"
style={{
textTransform: "none",
marginTop: 15,
}}
onClick={() => {
//setWorkflowLoading(true)
@@ -730,11 +749,11 @@ const WorkflowTemplatePopup = (props) => {
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
const boxHeight = 104
const boxHeight = visualOnly ? 75 : 104
const highlightColor = shownColor !== undefined && shownColor !== null && shownColor !== "" ? shownColor : "#f85a3e"
var hasInterest = false
if (userdata.interests !== undefined && userdata.interests !== null && userdata.interests.length > 0) {
if (userdata?.interests !== undefined && userdata?.interests !== null && userdata?.interests?.length > 0) {
const comparisonTitle = title === undefined || title === null ? "" : title.trim().toLowerCase().replaceAll(" ", "_")
for (var interestkey in userdata.interests) {
if (userdata.interests[interestkey].name === undefined || userdata.interests[interestkey].name === null || userdata.interests[interestkey].name === "") {
@@ -742,12 +761,12 @@ const WorkflowTemplatePopup = (props) => {
}
if (modalOpen) {
console.log("COMPARE: ", userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_"), comparisonTitle)
//console.log("COMPARE: ", userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_"), comparisonTitle)
}
if (userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_") === comparisonTitle) {
if (modalOpen) {
console.log("FOUND: ", comparisonTitle)
//console.log("FOUND: ", comparisonTitle)
}
hasInterest = true
@@ -791,7 +810,16 @@ const WorkflowTemplatePopup = (props) => {
}}
onClick={() => {
if (visualOnly === true) {
console.log("Not showing more than visuals.")
console.log("Not showing more than visuals. Workflow built: ", workflowBuilt, workflow)
if (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) {
window.open("/workflows/" + workflowBuilt, "_blank")
} else if (workflow.id !== undefined && workflow.id !== null && workflow.id !== "") {
window.open("/workflows/" + workflow.id, "_blank")
} else {
toast("Click 'Try this usecase' to generate workflows for this usecase.")
}
return
}
@@ -822,7 +850,7 @@ const WorkflowTemplatePopup = (props) => {
: null}
<div style={{ display: "flex", itemAlign: "left", textAlign: "left", }}>
<div style={{display: "flex", flex: 1, marginLeft: 25, marginTop: showTryitOut && !isActive ? 14 : 30, }}>
<div style={{display: "flex", flex: 1, marginLeft: 25, marginTop: visualOnly ? 18 : showTryitOut && !isActive ? 14 : 30, }}>
<div style={{zIndex: 51}}>
{img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ?
<Tooltip title={srcapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
@@ -849,7 +877,7 @@ const WorkflowTemplatePopup = (props) => {
}
</div>
<div style={{ marginLeft: 20, overflow: "hidden", maxHeight: 30, marginTop: showTryitOut && !isActive ? 8 : 23, }}>
<div style={{ marginLeft: 20, overflow: "hidden", maxHeight: 30, marginTop: visualOnly ? 12 : showTryitOut && !isActive ? 8 : 23, }}>
<Typography variant="body1" style={{ marginTop: parsedDescription.length === 0 ? 10 : 0, fontSize: isMobile ? 13 : 16, fontWeight: isHomePage ? 600 : null, textTransform: 'capitalize', color: isHomePage ? "var(--White-text, #F1F1F1)" : "rgba(241, 241, 241, 1)"}} >
<b>{parsedTitle}</b>
</Typography>
@@ -858,13 +886,19 @@ const WorkflowTemplatePopup = (props) => {
</div>
<div>
{isActive === true && errorMessage === "" ?
<Tooltip title="You already have workflows that are based on this usecase" placement="top">
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
</Tooltip>
visualOnly === true ?
<Tooltip title="Open the workflow in a new tab" placement="top">
<OpenInNewIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", top: 22, right: 20, }} />
</Tooltip>
:
<Tooltip title="You already have workflows that are based on this usecase" placement="top">
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
</Tooltip>
: ""}
{!isActive && hasInterest === true ?
{!isActive && hasInterest === true && !visualOnly ?
<Tooltip title="Your team has shown interest in this usecase previously." placement="top">
<InterestsIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: "rgba(254, 204, 0, 0.5)", top: 10, right: 10, }} />
</Tooltip>
@@ -872,7 +906,7 @@ const WorkflowTemplatePopup = (props) => {
</div>
{showTryitOut && !isActive ?
{showTryitOut && !isActive && !visualOnly ?
<Fade in={showTryitOut} timeout={300}>
<Button
variant="text"
@@ -898,4 +932,4 @@ const WorkflowTemplatePopup = (props) => {
)
}
export default WorkflowTemplatePopup
export default WorkflowTemplatePopup
@@ -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. Why?")
//console.log("Validation didn't run or get set for workflow. Why?")
return null
}
@@ -367,10 +379,13 @@ const WorkflowValidationTimeline = (props) => {
action.result = foundResult
action.status = foundResult.status
}
} else {
action.status = "SUCCESS"
}
}
const lastitem = index === relevantactions.length - 1
if (!lastitem) {
if (action.app_name === "Shuffle Tools") {
if (action.status === "SUCCESS") {
@@ -386,9 +401,10 @@ const WorkflowValidationTimeline = (props) => {
nodecolor = grey
branchcolor = grey
}
} else {
nodecolor = green
branchcolor = green
}
} else if (action.status === "SKIPPED") {
branchcolor = grey
} else {
@@ -489,7 +505,7 @@ const WorkflowValidationTimeline = (props) => {
if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) {
if (founderror.length > 0) {
middleError += founderror+"\n"
middleError += action.label+": "+founderror+"\n\n"
middleBranchColor = branchcolor
}
@@ -497,11 +513,18 @@ const WorkflowValidationTimeline = (props) => {
if (index === relevantactions.length-2 && relevantactions.length > 2) {
const selectedIcon = middleError.length > 0 ?
<Tooltip title={
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
{middleError}
</Typography>
}>
<Tooltip
title={
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
{middleError}
</Typography>
}
inputProps={{
paperProps: {
backgroundColor: "red",
}
}}
>
<IconButton style={{width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.0)", borderRadius: 30, marginTop: 2, }}>
<ErrorOutlineIcon style={{color: "red", }} />
</IconButton>
@@ -517,7 +540,7 @@ const WorkflowValidationTimeline = (props) => {
// Returns for anything non-middle
if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) {
if (founderror.length > 0) {
middleError += founderror+"\n"
middleError += action.label+": "+founderror+"\n\n"
}
return null
+649
View File
@@ -0,0 +1,649 @@
import { useEffect } from "react";
import React from "react";
import {
Typography,
Switch,
Button,
Tooltip,
TextField,
Grid,
Skeleton,
Dialog,
DialogTitle,
DialogContent,
Box,
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { Link } from "react-router-dom";
import theme from "../theme.jsx";
import { toast } from "react-toastify";
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{
const classes = useStyles();
const [show2faSetup, setShow2faSetup] = React.useState(false);
const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision)
const [ssoEntrypoint, setSsoEntrypoint] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_entrypoint === undefined ||
selectedOrganization.sso_config.sso_entrypoint.length === 0
? ""
: selectedOrganization.sso_config.sso_entrypoint
);
const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined
? false
: selectedOrganization.sso_config.SSORequired === undefined
? false
: selectedOrganization.sso_config.SSORequired);
const [ssoCertificate, setSsoCertificate] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_certificate === undefined ||
selectedOrganization.sso_config.sso_certificate.length === 0
? ""
: selectedOrganization.sso_config.sso_certificate
);
const [openidClientId, setOpenidClientId] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.client_id === undefined ||
selectedOrganization.sso_config.client_id.length === 0
? ""
: selectedOrganization.sso_config.client_id
);
const [openidClientSecret, setOpenidClientSecret] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.client_secret === undefined ||
selectedOrganization.sso_config.client_secret.length === 0
? ""
: selectedOrganization.sso_config.client_secret
);
const [openidAuthorization, setOpenidAuthorization] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_authorization === undefined ||
selectedOrganization.sso_config.openid_authorization.length === 0
? ""
: selectedOrganization.sso_config.openid_authorization
);
const [openidToken, setOpenidToken] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_token === undefined ||
selectedOrganization.sso_config.openid_token.length === 0
? ""
: selectedOrganization.sso_config.openid_token
)
useEffect(()=>{
if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
setOpenidClientSecret(selectedOrganization?.sso_config?.client_secret)
}
if (openidClientId !== selectedOrganization?.sso_config?.client_id) {
setOpenidClientId(selectedOrganization?.sso_config?.client_id)
}
if (openidAuthorization !== selectedOrganization?.sso_config?.openid_authorization) {
setOpenidAuthorization(selectedOrganization?.sso_config?.openid_authorization)
}
if (openidToken !== selectedOrganization?.sso_config?.openid_token) {
setOpenidToken(selectedOrganization?.sso_config?.openid_token)
}
if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate) {
setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate)
}
if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) {
setSsoEntrypoint(selectedOrganization?.sso_config?.sso_entrypoint)
}
if (SSORequired !== selectedOrganization?.sso_config?.SSORequired) {
setSSORequired(selectedOrganization?.sso_config?.SSORequired)
}
if (autoPrivision !== selectedOrganization?.sso_config?.auto_provision) {
setAutoProvision(selectedOrganization?.sso_config?.auto_provision)
}
},[selectedOrganization])
const orgSaveButton = (
<Tooltip title="Save any unsaved data" placement="bottom">
<Button
style={{ width: 244, height: 51, flex: 1, textTransform: 'capitalize', padding: "16px, 24px, 16px, 24px", borderRadius: 4, backgroundColor: "#ff8544", color: "#1a1a1a", fontSize: 16, }}
variant="contained"
color="primary"
disabled={
userdata === undefined ||
userdata === null ||
userdata.admin !== "true"
}
onClick={() =>
handleEditOrg(
selectedOrganization?.name,
selectedOrganization?.description,
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: !selectedOrganization?.defaults?.newsletter,
weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
},
{
sso_entrypoint: ssoEntrypoint,
sso_certificate: ssoCertificate,
client_id: openidClientId,
client_secret: openidClientSecret,
openid_authorization: openidAuthorization,
openid_token: openidToken,
SSORequired: SSORequired,
auto_provision: autoPrivision,
}
)
}
>
Save Changes
{/* <SaveIcon /> */}
</Button>
</Tooltip>
);
const toggleBetweenRequiredOrOptional = (event) => {
if (
ssoEntrypoint === "" &&
openidAuthorization === "" &&
openidToken === ""
) {
if (!SSORequired) {
toast.error(
"Please fill in fields for either OpenID connect or SSO before continuing. "
);
return;
}
} else {
toast.info("Toggled SSO. Remember to save.");
}
setSSORequired(event.target.checked);
};
const handleChangeAutoProvision = (event) => {
if (
ssoEntrypoint === "" &&
openidAuthorization === "" &&
openidToken === ""
) {
if (!autoPrivision) {
toast.error(
"Please fill in fields for either OpenID connect or SSO before continuing. "
);
return;
}
} else {
setAutoProvision((prev)=> !prev);
toast.info("Toggled Auto Provisioning. Remember to save.");
}
};
const HandleTestSSO = () => {
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
const data = {
org_id: selectedOrganization?.id,
sso_test: true,
};
fetch(url, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify(data),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status !== 200) {
toast.error(
"Failed to test SSO. Please try again later or contact support@shuffler.io if issue persists.",
{ duration: 3000 }
);
return null;
}
return response.json();
})
.then((responjson) => {
if (!responjson) return;
if (responjson["reason"] === "SSO_REDIRECT") {
toast.info(
"Redirecting to SSO login page as SSO is required for this organization.",
{
duration: 3000,
onClose: () => {
window.location.href = responjson["url"];
}
}
);
} else {
toast.error(
"No SSO found for this org. Please set up SSO for this org.",
{ duration: 3000 }
);
}
})
.catch((error) => {
console.error("Error for SSO test:", error);
toast.error(
"An error occurred while testing SSO. Please try again.",
{ duration: 3000 }
);
});
};
return (
<div style={{ width: "100%", height: "100%",boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: '#212121', borderRadius: '16px', }}>
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}} >
<div style={{ width: "100%", overflowX: 'hidden', maxWidth: 883}}>
<Typography style={{ width: "100%", fontWeight: 'bold', fontSize: 24}}>
SSO Configuration
</Typography>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
justifyContent: 'flex-start',
marginTop: 20
}}
>
<Typography
variant="body2"
color="textSecondary"
style={{ marginTop: 5, marginBottom: 5, color: "rgba(158, 158, 158, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400 }}
>
Make SAML SSO or OpenID Authentication Required or Optional for Your Organization.
</Typography>
<div>
<Switch
checked={SSORequired}
onChange={toggleBetweenRequiredOrOptional}
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
/>
{SSORequired ? "Required" : "Optional"}
</div>
</div>
{/* auto privisiong in sso */}
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
justifyContent: 'flex-start',
marginTop: 30
}}
>
<Typography
variant="body2"
color="textSecondary"
style={{ marginTop: 5, marginBottom: 5, color: "rgba(158, 158, 158, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400 }}
>
Auto-provisioning of users in SSO. By default, users are auto-provisioned in SSO when they login. If you enable this, no new user will be added in your organization when they login via SSO.
</Typography>
<div>
<Switch
checked={autoPrivision}
onChange={handleChangeAutoProvision}
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
title="Disable auto-provisioning of users in SSO"
/>
</div>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 30,
width: "100%",
paddingBottom: 10,
}}
>
<Typography style={{color: "rgba(158, 158, 158, 1)", margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
You can test your SSO configuration by clicking the button below.
Before testing, ensure you have set Open ID Connect or SAML SSO
credentials.
</Typography>
<Tooltip
title={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
? "Please ensure all SSO credentials are set before testing."
: ""
}
>
<span style={{ width: 100 }}>
<Button
variant="outlined"
color="primary"
style={{ width: 100, textTransform: "none", margin: "10px 10px 10px 0px" }}
disabled={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
}
onClick={HandleTestSSO}
>
Test SSO
</Button>
</span>
</Tooltip>
</div>
<Grid item xs={12} sx={{marginTop: 2}}>
<span style={{ display: "flex", flexDirection: "column" }}>
<Typography style={{ textAlign: "left", color: "rgba(241, 241, 241, 1)", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: "bold", }}>OpenID connect</Typography>
<span style={{ marginTop: 8, color: "rgba(158, 158, 158, 1)", fontSize: 16, fontWeight: 400 }}>
Configure and Authorize SAML / SSO or OpenID connect. {" "}
<a
target="_blank"
href="/docs/extensions#single-signon"
style={{ color: "rgba(255, 132, 68, 1)" }}
>
Learn more
</a>
</span>
</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>
<Grid container style={{ marginTop: 8, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>Client ID</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
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);
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
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={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>Client Secret</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
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 secret - DONT use this if dealing with implicit auth / PKCE"
value={openidClientSecret}
onChange={(e) => {
setOpenidClientSecret(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
color: "white",
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
},
}}
/>
</span>
</Grid>
</Grid>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>Authorization URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID authorization URL (usually ends with /authorize)"
value={openidAuthorization}
onChange={(e) => {
setOpenidAuthorization(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
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={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>Token URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID token URL (usually ends with /token)"
value={openidToken}
onChange={(e) => {
setOpenidToken(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
color: "white",
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
},
}}
/>
</span>
</Grid>
</Grid>
</Grid>
{/**/}
{/*isCloud ? null : */}
<Grid item xs={12} sx={{ marginTop: 3.5 }} >
<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>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>SSO Entrypoint (IdP)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
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 entrypoint URL from your provider"
value={ssoEntrypoint}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
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={{}}>
<span>
<Typography style={{ color: "rgba(255, 255, 255, 1)", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontWeight: 400, fontSize: 16 }}>SSO Certificate (X509)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: isEditOrgTab ? "rgba(33, 33, 33, 1)" : theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The X509 certificate to use"
value={ssoCertificate}
onChange={(e) => {
setSsoCertificate(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
style: {
color: "white",
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
fontWeight: 400,
fontSize: 16,
borderRadius: 4,
},
}}
/>
</span>
</Grid>
</Grid>
</Grid>
<div style={{ textAlign: "center", margin: "50px auto 0px auto", }}>
{orgSaveButton}
</div>
</div>
</div>
</div>
)
}
export default SSOTab
+39 -6
View File
@@ -58,7 +58,7 @@ const data = [
"curve-style": "unbundled-bezier",
label: "data(label)",
"text-margin-y": "-15px",
width: "3px",
width: "2px",
color: "white",
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
@@ -95,6 +95,9 @@ const data = [
{
selector: `node[type="COMMENT"]`,
css: {
label: function(element) {
return element.data("label")
},
shape: "roundrectangle",
color: "data(color)",
width: "data(width)",
@@ -128,10 +131,8 @@ const data = [
selector: `node[example="noapp"]`,
css: {
// Make background image padding on the left side 20px
"background-width": "75%",
"background-height": "75%",
"background-position-x": "17px",
"background-position-y": "17px",
"background-width": "100%",
"background-height": "100%",
"background-color": "data(iconBackground)",
"background-fill": "data(fillstyle)",
@@ -156,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: {
@@ -423,7 +456,7 @@ const data = [
{
selector: "edge.success-highlight",
css: {
width: "5px",
width: "3px",
"target-arrow-color": "#41dcab",
"line-color": "#41dcab",
"transition-property": "line-color, width",
+49 -13
View File
@@ -14,7 +14,7 @@ const theme = createTheme(adaptV4Theme({
contrastText: "#000000",
},
text: {
secondary: "rgba(255,255,255,0.7)",
secondary: "rgba(255,255,255,0.8)",
},
type: "dark",
//inputColor: "#383B40",
@@ -24,6 +24,7 @@ const theme = createTheme(adaptV4Theme({
//platformColor: "#1c1c1d",
platformColor: "#212121",
backgroundColor: "#1a1a1a",
distributionColor: "#40E0D0",
green: "#5cc879",
borderRadius: 10,
@@ -75,10 +76,18 @@ const theme = createTheme(adaptV4Theme({
fontSize: 11,
},
defaultImage: "/images/no_image.png",
singulOrange: "/images/singul_orange.png",
singulGreen: "/images/singul_green.png",
singulBlackWhite: "/images/singul_black_white.png",
},
typography: {
fontFamily: `"Roboto", "Helvetica", "Arial", "inter", sans-serif`,
useNextVariants: true,
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightSemiBold: 600,
fontWeightBold: 700,
h1: {
fontSize: 40,
},
@@ -100,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');
}
`,
},
},
},
}));
+12 -24
View File
@@ -11,29 +11,16 @@ const Admin2 = (props) => {
const [organizationFeatures, setOrganizationFeatures] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
if (document !== undefined) {
if (selectedOrganization?.name !== undefined) {
document.title = selectedOrganization?.name + " - Admin - Shuffle"
} else {
document.title = "Admin - Shuffle"
}
}
const handleGetOrg = (orgId) => {
// if (
// serverside !== true &&
// window.location.search !== undefined &&
// window.location.search !== null
// ) {
// const urlSearchParams = new URLSearchParams(window.location.search);
// const params = Object.fromEntries(urlSearchParams.entries());
// const foundorgid = params["org_id"];
// if (foundorgid !== undefined && foundorgid !== null) {
// orgId = foundorgid;
// }
// }
console.log("getting organization details for: ", orgId);
// if (orgId === undefined) {
// toast(
// "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.",
// );
// return;
// }
// Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
@@ -124,7 +111,8 @@ const Admin2 = (props) => {
setSelectedStatus(leads);
}
setSelectedOrganization(responseJson);
setSelectedOrganization(responseJson)
var lists = {
active: {
triggers: [],
@@ -330,7 +318,7 @@ const Admin2 = (props) => {
return (
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 29, zoom: 0.9}}>
<AdminNavBar userdata={userdata} isLoaded={isLoaded} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} selectedTab={selectedTab} orgId={selectedOrganization.id} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} setNotifications={setNotifications} stripeKey={stripeKey} notifications={notifications} checkLogin={checkLogin} globalUrl={globalUrl} isCloud={isCloud} serverside={serverside} />
<AdminNavBar userdata={userdata} isLoaded={isLoaded} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} selectedTab={selectedTab} orgId={selectedOrganization.id} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} setNotifications={setNotifications} stripeKey={stripeKey} notifications={notifications} checkLogin={checkLogin} globalUrl={globalUrl} isCloud={isCloud}/>
</div>
);
};
File diff suppressed because one or more lines are too long
+79 -19
View File
@@ -95,14 +95,18 @@ const ApiExplorerWrapper = (props) => {
};
useEffect(() => {
if (openapi?.id === "HTTP") {
selectedAppData.name = "HTTP"
}
if (selectedAppData !== undefined && selectedAppData !== null && Object.getOwnPropertyNames(selectedAppData).length > 0) {
HandleAppAuthentication(selectedAppData?.name)
}
}, [selectedAppData, openapi])
useEffect(() => {
getAppData(appid)
if (appid !== undefined && appid !== null && appid.length !== 0) {
getAppData(appid)
HandleGetLocations()
}
@@ -136,7 +140,10 @@ const ApiExplorerWrapper = (props) => {
const runAlgoliaAppSearch = (appname) => {
const index = searchClient.initIndex("appsearch");
console.log("Running appsearch for: ", appname);
if (appname === "HTTP" || appname === "http") {
navigate("/apis")
return
}
index
.search(appname)
@@ -151,19 +158,25 @@ const ApiExplorerWrapper = (props) => {
if (newname?.includes(appsearchname)) {
found = true
getAppData(hit.objectID)
break
}
}
if (!found) {
toast.error("Failed to get app data or App doesn't exist (1). Redirecting..");
toast.error(`Failed to get API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
}
} else {
toast.error("Failed to get app data or App doesn't exist (2). Redirecting..");
toast.error(`Failed to get API data for '${appname}' (2). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
@@ -177,6 +190,29 @@ const ApiExplorerWrapper = (props) => {
// Fetch data when appid is available
const getAppData = useCallback((appid) => {
if (appid === undefined || appid === null || appid.length === 0) {
toast.warning("No app ID loaded. Showing default API testing window. ")
setOpenapi({
"id": "HTTP",
"servers": [
{"url": "https://shuffler.io"},
],
"info": {
"title": "HTTP",
"x-logo": theme.palette?.defaultImage,
},
"paths": {
"/api/v1/workflows/usecases": {
"get": {
"summary": "Custom Action",
}
}
}
})
setAppLoaded(true)
//setTimeout(() => {
// navigate("/search?tab=apps")
//}, 3000)
return
}
@@ -197,16 +233,21 @@ const ApiExplorerWrapper = (props) => {
.then((response) => {
if (response.status !== 200) {
toast.error("Failed to get app data or App doesn't exist (3). Redirecting..");
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
setTimeout(() => {
navigate("/search?tab=apps")
}, 3000)
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
handleDecodeOfOpenApiData(responseJson);
if (responseJson.openapi === undefined || responseJson.openapi === null) {
toast.warning("Loaded App, but no API found. Redirecting back to app..")
navigate(`/apps/${appid}`)
} else {
handleDecodeOfOpenApiData(responseJson);
}
} else {
toast.error("Failed to get app data or App doesn't exist (4)");
}
@@ -337,7 +378,7 @@ const ApiExplorerWrapper = (props) => {
const parsedHeaders = {};
if (typeof headers === 'string' && headers) {
const splitHeaders = headers.split("\n");
const splitHeaders = headers.split(`\n`)
splitHeaders.forEach(header => {
let splitItem;
@@ -399,6 +440,13 @@ const ApiExplorerWrapper = (props) => {
selectedAppData.name = appname
}
console.log("APPNAME: ", appname, openapi.id)
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
setAppAuthentication(data)
setSelectedAuthentication({})
return
}
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid || appAuth?.app?.name?.replaceAll(" ", "_").toLowerCase() === selectedAppData?.name?.replaceAll(" ", "_").toLowerCase());
if (filteredData.length === 0) {
setAppAuthentication([])
@@ -481,7 +529,7 @@ const ApiExplorerWrapper = (props) => {
return array
.map(item => (item.key.trim().length > 0 && item.value.trim().length > 0 ? `${item.key}=${item.value}` : ""))
.filter(str => str.length > 0)
.join("\n");
.join(``);
};
var appid = "";
@@ -491,7 +539,7 @@ const ApiExplorerWrapper = (props) => {
}else if (openapi?.id?.length > 0) {
appid = openapi?.id;
}else{
toast.error("App id is missing. Please try again.");
toast.error("App id is missing and we can't run the API. Please contact support@shuffler.io if this persists.");
return;
}
@@ -615,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.")
@@ -678,6 +726,7 @@ const ApiExplorerWrapper = (props) => {
'& .MuiList-root': {
backgroundColor: "#1f1f1f",
},
maxWidth: 500,
},
}
}}
@@ -709,6 +758,7 @@ const ApiExplorerWrapper = (props) => {
No Selection
</MenuItem>
<Divider />
{appAuthentication?.length > 0 ? appAuthentication.map((appAuth) => (
<div
key={appAuth?.id}
@@ -718,13 +768,13 @@ const ApiExplorerWrapper = (props) => {
backgroundColor: '#1f1f1f',
color: 'white',
padding: '5px',
textAlign: "left",
}}
>
<MenuItem
value={appAuth?.id}
sx={{
display: 'flex',
justifyContent: 'space-between',
color: 'white',
}}
onClick={(e) => {
@@ -732,6 +782,11 @@ const ApiExplorerWrapper = (props) => {
setAuthenticationName(appAuth.app?.name)
}}
>
{appAuth?.app?.large_image !== undefined && appAuth?.app?.large_image !== null && appAuth?.app?.large_image.length > 0 ?
<Tooltip title={appAuth?.app?.name} placement="top">
<img src={appAuth?.app?.large_image} alt={appAuth?.app?.name} style={{ maxWidth: 20, maxHeight: 20, marginRight: 10, borderRadius: 5 }} />
</Tooltip>
: null}
{appAuth?.validation?.valid === true ?
<Tooltip title="Validated" placement="top">
<CheckCircleIcon style={{ color: green, marginRight: 10, }} />
@@ -1631,7 +1686,9 @@ const ApiExplorerWrapper = (props) => {
>
<div style={{display: "flex", }}>
<div style={{width: 600, margin: "auto", display: "flex", }}>
{isLoggedIn === true ?
{openapi?.id === "HTTP" ?
null
: isLoggedIn === true ?
<Button
variant={authHighlighted ? "contained" : "outlined"}
style={{
@@ -1667,17 +1724,20 @@ const ApiExplorerWrapper = (props) => {
}
{appAuthentication?.length > 0 ?
<div style={{display: "flex", flex: 2, }}>
<Typography style={{textAlign: "center", flex: 1, color: "white", marginTop: 15, }} variant="body1">
or use
</Typography>
<div style={{display: "flex", flex: 2, textAlign: "center", }}>
{openapi?.id === "HTTP" ? null :
<Typography style={{textAlign: "center", flex: 1, color: "white", marginTop: 15, }} variant="body1">
or use
</Typography>
}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 'auto',
margin: 'auto',
maxWidth: 200,
}}>
<AuthenticationList />
</div>
+90 -27
View File
@@ -276,7 +276,14 @@ export const appCategories = [
"color": "#FFC107",
"icon": "network",
"action_labels": ["Get Rules", "Allow IP", "Block IP",],
}, {
},
{
"name": "AI",
"color": "#FFC107",
"icon": "AI",
"action_labels": ["Answer Question", "Run Action", "Run LLM",],
},
{
"name": "Other",
"color": "#FFC107",
"icon": "other",
@@ -418,6 +425,7 @@ const AppCreator = (defaultprops) => {
const [appBuilding, setAppBuilding] = useState(false);
const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false);
const [actionAmount, setActionAmount] = useState(increaseAmount);
const [newAppGroup, setNewAppGroup] = useState("")
const [oauth2Scopes, setOauth2Scopes] = useState([]);
const [oauth2Type, setOauth2Type] = useState("delegated");
@@ -2616,10 +2624,26 @@ const AppCreator = (defaultprops) => {
credentials: "include",
})
.then((response) => {
//if (response.status !== 200) {
// setErrorCode("An error occurred during validation")
// throw new Error("NOT 200 :O")
//}
if (response.status === 403) {
var urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("id")) {
toast.error("Please log in to build this app. If this error persists, please contact support@shuffler.io")
} else {
toast.error("Failed to save the app as you are not the owner. Redirecting you to the forking page. When there, save again.")
if (props.match.params.appid !== undefined && props.match.params.appid !== null && props.match.params.appid.length > 0) {
setTimeout(() => {
window.open(`/apps/new?id=${props.match.params.appid}`, "_blank")
}, 2500)
}
}
return
}
if (response.status !== 200) {
setErrorCode("An error occurred during validation")
//throw new Error("NOT 200 :O")
}
setAppBuilding(false);
return response.json();
@@ -3167,6 +3191,12 @@ const AppCreator = (defaultprops) => {
setOauth2Scopes(chips)
setUpdate(Math.random())
}}
onBlur={(e) => {
var newchips = oauth2Scopes
newchips.push(e.target.value)
setOauth2Scopes(newchips)
setUpdate(Math.random())
}}
/>
</div>
) : null;
@@ -4616,23 +4646,25 @@ const AppCreator = (defaultprops) => {
}}
/>
*/}
<h4>Choose a Category</h4>
<Select
fullWidth
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
onChange={(e) => {
setNewWorkflowCategories([e.target.value]);
setUpdate("added " + e.target.value);
}}
value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
style={{ backgroundColor: inputColor, color: "white", height: "50px" }}
>
{categories.map((data, index) => {
<div style={{display: "flex", }}>
<div style={{flex: 2, }}>
<h4>Choose a Category</h4>
<Select
fullWidth
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
onChange={(e) => {
setNewWorkflowCategories([e.target.value]);
setUpdate("added " + e.target.value);
}}
value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
style={{ backgroundColor: inputColor, color: "white", height: "50px" }}
>
{categories.map((data, index) => {
if (data === undefined || data === null || data === "" || data === undefined || data === null || data === "") {
return null
}
@@ -4645,9 +4677,38 @@ const AppCreator = (defaultprops) => {
>
{data.name}
</MenuItem>
)
)
})}
</Select>
</Select>
</div>
{/*
<div style={{flex: 1, marginLeft: 25, }}>
<Tooltip title="Helps when searching for apps. E.g. Google or Microsoft are their own groups." placement="top">
<h4 style={{marginBottom: 0, }}>Group</h4>
</Tooltip>
<TextField
style={{
}}
fullWidth
placeholder="Group"
type="text"
id="standard-required"
margin="normal"
variant="outlined"
defaultValue={newAppGroup}
onChange={(e) => {
setNewAppGroup(e.target.value)
setUpdate("group added "+e.target.value)
}}
InputProps={{
style: {
color: "white",
},
}}
/>
</div>
*/}
</div>
<h4>Tags</h4>
<MuiChipsInput
style={{ marginTop: 10 }}
@@ -5435,6 +5496,7 @@ const AppCreator = (defaultprops) => {
minWidth: 174,
minHeight: 174,
objectFit: "contain",
borderRadius: theme.palette?.borderRadius,
}}
/>
);
@@ -5450,6 +5512,7 @@ const AppCreator = (defaultprops) => {
margin: "auto",
marginTop: 30,
marginLeft: 40,
borderRadius: theme.palette?.borderRadius,
}}
onClick={() => {
upload.click();
@@ -5832,6 +5895,7 @@ const AppCreator = (defaultprops) => {
//setOpenApiModal(true)
toast.info("Action merging & fork management coming soon")
}}
disabled={true}
style={{marginLeft: 10, }}
>
<CallMergeIcon
@@ -5846,7 +5910,7 @@ const AppCreator = (defaultprops) => {
href="https://shuffler.io/docs/app_creation#app-creator-instructions"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Click here to learn more about app creation
Click to learn more about app creation
</a>
<div
style={{
@@ -5940,7 +6004,6 @@ const AppCreator = (defaultprops) => {
<TextField
required
style={{
paddingTop: 5,
marginTop: 5,
marginRight: 15,
backgroundColor: inputColor,
@@ -6196,7 +6259,7 @@ const AppCreator = (defaultprops) => {
{testView}
*/}
<div style={{height: 50, padding: 15, display: "flex", marginTop: 35, position: "fixed", bottom: 0, left: 0, width: "100%", backgroundColor: theme.palette?.backgroundColor, borderTop: "1px solid rgba(255,255,255,0.3)",}}>
<div style={{height: isCloud ? 50 : 80, padding: 15, display: "flex", marginTop: 35, position: "fixed", bottom: 0, left: 0, width: "100%", backgroundColor: theme.palette?.backgroundColor, borderTop: "1px solid rgba(255,255,255,0.3)",}}>
<div style={{width: 450, margin: "auto", display: "flex", textAlign: "center", }}>
{appDownloadData.length > 0 ?
<Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
File diff suppressed because it is too large Load Diff
+7 -5
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]);
@@ -1258,7 +1261,6 @@ const Apps = (props) => {
style={{
width: 150,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: inputColor,
color: "white",
height: 35,
marginleft: 10,
+48 -12
View File
@@ -28,6 +28,7 @@ import {
Close as CloseIcon,
Cached as CachedIcon,
CloudDownload as CloudDownloadIcon,
ForkRight as ForkRightIcon,
} from "@mui/icons-material";
import InputAdornment from '@mui/material/InputAdornment';
@@ -48,11 +49,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}`;
var canEditApp = userdata.admin === "true" || userdata.id === data?.owner || data?.owner === "" || (userdata.admin === "true" && userdata.active_org.id === data?.reference_org) || !data?.generated
//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)
const paperStyle = {
backgroundColor: mouseHoverIndex === index ? "rgba(26, 26, 26, 1)" : "#212121",
@@ -66,7 +70,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}>
@@ -128,6 +132,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
margin: "12px 0",
fontFamily: theme?.typography?.fontFamily,
}}>
<div style={{
display: 'flex',
flexDirection: 'row',
@@ -139,6 +144,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
maxWidth: "90%",
gap: 8
}}>
<div style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -147,6 +153,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",
@@ -157,6 +164,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
}}>
{data.categories ? data.categories.join(", ") : "NA"}
</div>
<div style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -182,8 +190,9 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
</span>
))}
</div>
{/* Deactivate button */}
{currTab === 0 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && (
{(currTab === 0 || currTab === 1) && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && (
<div style={{
display: "flex",
gap: 8,
@@ -191,7 +200,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
paddingRight: 20
}}>
{
canEditApp && (
canEditApp ? (
<button style={{ backgroundColor: "rgba(73, 73, 73, 1)", border: "none", cursor: "pointer", color: "white", borderRadius: 3, display: "flex", alignItems: "center", justifyContent: "center", height: 35 }}
onClick={(event) => {
event.preventDefault();
@@ -204,9 +213,22 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
>
<EditIcon />
</button>
) : (
<button style={{ backgroundColor: "rgba(73, 73, 73, 1)", border: "none", cursor: "pointer", color: "white", borderRadius: 3, display: "flex", alignItems: "center", justifyContent: "center", height: 35 }}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
const editUrl = "/apps/new?id=" + data?.id;
navigate(editUrl)
}}
>
<ForkRightIcon />
</button>
)
}
<Button
disabled={data?.reference_org === userdata?.active_org?.id}
className="deactivate-button"
sx={{
width: 110,
@@ -228,7 +250,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: {
@@ -240,11 +262,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());
@@ -613,6 +638,7 @@ const Hits = ({
</div>
)}
</div>
<div style={{
display: 'flex',
justifyContent: 'flex-end',
@@ -622,7 +648,7 @@ const Hits = ({
}}>
{hoverEffect === index && (
<div>
{allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? (
{allActivatedAppIds && allActivatedAppIds?.includes(data.objectID) ? (
<Button
style={{
width: 110,
@@ -641,7 +667,9 @@ const Hits = ({
}}>
Deactivate
</Button>
) : (
<Button
style={{
backgroundColor: "#FF8544",
@@ -1853,6 +1881,7 @@ const Apps2 = (props) => {
app={selectedApp}
userdata={userdata}
globalUrl={globalUrl}
getApps={getApps}
/>
<AppCreationModal
open={createAppModalOpen}
@@ -2228,6 +2257,10 @@ const Apps2 = (props) => {
leftSideBarOpenByClick={leftSideBarOpenByClick}
userdata={userdata}
fetchApps={fetchApps}
setUserApps={setUserApps}
appsToShow={appsToShow}
setAppsToShow={setAppsToShow}
/>
))}
</div>
@@ -2277,6 +2310,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>
+1 -1
View File
@@ -397,7 +397,7 @@ const Dashboard = (props) => {
if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch);
const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem)
}
+27 -108
View File
@@ -100,7 +100,7 @@ export const CopyToClipboard = (props) => {
)
}
export const Paragrah = (props) => {
export const Paragraph = (props) => {
const element = React.createElement(
`p`,
{},
@@ -123,7 +123,7 @@ export const Paragrah = (props) => {
return (
<div>
<div>
{element}
</div>
)
@@ -184,15 +184,15 @@ export const CodeHandler = (props) => {
if (validate.valid === false) {
// Check if https://shuffler.io in the url
// if so, then we change it for the current url
if (propvalue.includes("https://shuffler.io")) {
newprop = propvalue.replace("https://shuffler.io", window.location.origin)
}
if (propvalue.includes("https://shuffler.io/api")) {
newprop = propvalue.replace("https://shuffler.io/api", window.location.origin+"/api")
// Check if it contains Bearer APIKEY
// If so, replace apikey
//if (newprop.includes("Bearer APIKEY")) {
// newprop = newprop.replace("Bearer APIKEY", "Bearer API
//}
const foundurl = localStorage.getItem("globalUrl")
if (foundurl !== undefined && foundurl !== null && foundurl !== "") {
newprop = propvalue.replace("https://shuffler.io/api", foundurl+"/api")
}
}
}
// Need to check if it's singletick or multi
@@ -383,6 +383,7 @@ const Docs = (defaultprops) => {
if (hash.includes('?')) {
hash = hash.split('?')[0]
}
if (hash) {
const element = document.getElementById(hash.toLowerCase())
if (element) {
@@ -526,7 +527,7 @@ const Docs = (defaultprops) => {
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette?.borderRadius,
marginBottom: 30,
marginBottom: 25,
display: "flex",
}}
>
@@ -618,7 +619,8 @@ const Docs = (defaultprops) => {
<Divider
style={{
width: "90%",
marginTop: 40,
marginTop: 60,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
@@ -664,6 +666,8 @@ const Docs = (defaultprops) => {
minHeight: "93vh",
maxHeight: "93vh",
marginTop: 70,
maxWidth: 250,
overflow: "hidden",
}
const fetchDocList = () => {
@@ -705,13 +709,12 @@ const Docs = (defaultprops) => {
// 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, "");
const newdata = responseJson.reason.replace(imgRegex, '![]($1)').replace(tocRegex, "");
setData(newdata);
if (docId === undefined) {
document.title = "Shuffle documentation introduction";
document.title = "Shuffle automation documentation";
} else {
document.title = "Shuffle " + docId + " documentation";
document.title = "Shuffle " + docId.replace("_", " ") + " documentation";
}
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
@@ -782,88 +785,6 @@ const Docs = (defaultprops) => {
fetchDocs(props.match.params.key);
}
// const parseElementScroll = () => {
// const offset = 45;
// var parent = document.getElementById("markdown_wrapper_outer");
// if (parent !== null) {
// //console.log("IN PARENT")
// var elements = parent.getElementsByTagName("h2");
//
// const name = window.location.hash
// .slice(1, window.location.hash.length)
// .toLowerCase()
// .split("%20")
// .join(" ")
// .split("_")
// .join(" ")
// .split("-")
// .join(" ")
// .split("?")[0]
//
// //console.log(name)
// var found = false;
// for (var key in elements) {
// const element = elements[key];
// if (element.innerHTML === undefined) {
// continue;
// }
//
// // Fix location..
// if (element.innerHTML.toLowerCase() === name) {
// //console.log(element.offsetTop)
// element.scrollIntoView({ behavior: "smooth" });
// //element.scrollTo({
// // top: element.offsetTop+offset,
// // behavior: "smooth"
// //})
// found = true;
// //element.scrollTo({
// // top: element.offsetTop-100,
// // behavior: "smooth"
// //})
// }
// }
//
// // H#
// if (!found) {
// elements = parent.getElementsByTagName("h3");
// //console.log("NAMe: ", name)
// found = false;
// for (key in elements) {
// const element = elements[key];
// if (element.innerHTML === undefined) {
// continue;
// }
//
// // Fix location..
// if (element.innerHTML.toLowerCase() === name) {
// element.scrollIntoView({ behavior: "smooth" });
// //element.scrollTo({
// // top: element.offsetTop-offset,
// // behavior: "smooth"
// //})
// found = true;
// //element.scrollTo({
// // top: element.offsetTop-100,
// // behavior: "smooth"
// //})
// }
// }
// }
// }
// //console.log(element)
//
// //console.log("NAME: ", name)
// //console.log(document.body.innerHTML)
// // parent = document.getElementById(parent);
//
// //var descendants = parent.getElementsByTagName(tagname);
//
// // this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' });
//
// //$(".parent").find("h2:contains('Statistics')").parent();
// };
const markdownStyle = {
color: "rgba(255, 255, 255, 0.90)",
overflow: "hidden",
@@ -872,7 +793,7 @@ const Docs = (defaultprops) => {
maxWidth: "100%",
minWidth: "100%",
overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1.1rem",
fontSize: isMobile ? "1.3rem" : "1rem",
};
const alertNote = {
@@ -1014,7 +935,7 @@ const Docs = (defaultprops) => {
h5: Heading,
h6: Heading,
a: OuterLink,
p: Paragrah,
p: Paragraph,
blockquote: Blockquote,
}
@@ -1084,25 +1005,23 @@ const Docs = (defaultprops) => {
<div style={IndexBar}>
{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>
{tocLines.map((data, index) => {
return (
<div className="toc">
<div className="toc"
>
<ListItemButton
key={data.text}
href={`#${data.id}`}
style={{
color: activeId === data.id ? "#f86a3e" : "inherit",
textDecoration: "none",
fontSize: "14px",
fontWeight: 400,
padding: "4px 0",
paddingLeft: "8px",
paddingRight: "8px",
padding: "4px 8px 4px 8px",
lineHeight: "20px",
color: activeId === data.id ? "#f86a3e" : "rgba(255,255,255,0.6)",
}}
onClick={(e) => {
handleCollapse(index)
@@ -1296,7 +1215,7 @@ const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{
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) ? 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}
-10
View File
@@ -80,25 +80,15 @@ const useStyles = makeStyles((theme) => ({
datagrid: {
border: 0,
"& .MuiDataGrid-columnsContainer": {
backgroundColor:
theme.palette.type === "light" ? "#fafafa" : theme.palette.inputColor,
},
"& .MuiDataGrid-iconSeparator": {
display: "none",
},
"& .MuiDataGrid-colCell, .MuiDataGrid-cell": {
borderRight: `1px solid ${
theme.palette.type === "light" ? "white" : "#303030"
}`,
},
"& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell": {
borderBottom: `1px solid ${
theme.palette.type === "light" ? "#f0f0f0" : "#303030"
}`,
},
"& .MuiDataGrid-cell": {
color:
theme.palette.type === "light" ? "white" : "rgba(255,255,255,0.65)",
},
"& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption":
{
+6 -3
View File
@@ -170,6 +170,9 @@ const LoginDialog = (props) => {
);
setMFAField(true);
return;
} else if (responseJson["reason"] === "MFA_SETUP") {
window.location.href = `/login/${responseJson.url}/mfa-setup`;
return;
}
setLoginInfo("Successful login, rerouting");
@@ -183,9 +186,9 @@ const LoginDialog = (props) => {
if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) {
console.log("RUN Welcome!!")
setTimeout(() => {
navigate("/welcome?tab=2")
},200)
setTimeout(() => {
navigate("/welcome?tab=2")
},200)
// window.location.pathname = ""
return
}
+30 -8
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
@@ -117,7 +135,7 @@ const RunWorkflow = (defaultprops) => {
props.match = {}
props.match.params = params
const defaultTitle = workflow.name !== undefined ? "Shuffle - Form for " + workflow.name : "Shuffle - Form to Run Workflows"
const defaultTitle = workflow.name !== undefined ? "Form for " + workflow.name : "Shuffle - Form to Run Workflows"
if (document != undefined && document.title != defaultTitle) {
document.title = defaultTitle
}
@@ -152,7 +170,7 @@ const RunWorkflow = (defaultprops) => {
}
}
console.log("EXEC: ", executionArgument)
//console.log("EXEC: ", executionArgument)
for (var key in executionArgument) {
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
return false
@@ -705,6 +723,8 @@ const RunWorkflow = (defaultprops) => {
break
}
}
} else {
setInputQuestions(responseJson.input_questions)
}
if (responseJson.form_control.input_markdown !== undefined && responseJson.form_control.input_markdown !== null && responseJson.form_control.input_markdown.length > 0) {
@@ -894,7 +914,6 @@ const RunWorkflow = (defaultprops) => {
const fetchUpdates = (execution_id, authorization, getorg, replaceMarkdown) => {
if (execution_id === undefined || execution_id === null || execution_id === "") {
console.log("No execution id: ", execution_id)
stop()
return
}
@@ -1225,9 +1244,9 @@ const RunWorkflow = (defaultprops) => {
</div>
}
{workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ?
{workflow?.input_questions !== undefined && workflow?.input_questions !== null && workflow?.input_questions?.length > 0 ?
<div style={{marginBottom: 5, }}>
{inputQuestions.map((question, index) => {
{inputQuestions?.map((question, index) => {
// Multiple choice checks for semicolon-splits
var multiChoiceOptions = question.value !== undefined && question.value !== null && question.value.length > 0 && question.value.includes(";") ? question.value.split(";") : []
@@ -1241,9 +1260,12 @@ const RunWorkflow = (defaultprops) => {
return (
<div style={{marginBottom: 10}} key={index}>
<Typography variant="body2" color="textSecondary">
{question.name}
</Typography>
{multiChoiceOptions.length > 1 ?
<div>
{question.name}
<Select
disabled={disabledButtons}
fullWidth
@@ -1280,7 +1302,7 @@ const RunWorkflow = (defaultprops) => {
backgroundColor: theme.palette.inputColor,
marginTop: 5,
}}
label={question.value.charAt(0).toUpperCase() + question.value.slice(1)}
label={question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)}
required
disabled={disabledButtons}
@@ -1642,7 +1664,7 @@ const RunWorkflow = (defaultprops) => {
maxHeight: 500,
position: "absolute",
left: 150,
top: 0,
top: 75,
border: "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette?.borderRadius,
+2
View File
@@ -719,6 +719,7 @@ const Settings = (props) => {
//onChange={e => setUsername(e.target.value)}
/>
</div>
{/*
<div style={{ flex: "1", display: "flex", flexDirection: "row" }}>
<TextField
style={{
@@ -771,6 +772,7 @@ const Settings = (props) => {
onChange={(e) => setLastname(e.target.value)}
/>
</div>
*/}
<h2>APIKEY</h2>
<a
target="_blank"
+1 -1
View File
@@ -148,7 +148,7 @@ const SetAuthentication = (props) => {
console.log("App: ", app)
return (
<div style={{width: 1000, margin: "auto", marginTop: 50, }}>
<div style={{width: 1000, margin: "auto", paddingTop: 50, }}>
{loadFail !== "" ?
loadFail
:
+195 -75
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", }}>
@@ -621,7 +800,7 @@ const UsecaseListComponent = (props) => {
parsedUsecase.dstapp = newsubcase.dstapp
var workflowBuilt = false
var workflowBuilt = ""
const newname = subcase.name.toLowerCase().replaceAll(" ", "_")
for (var workflowkey in workflows) {
const workflow = workflows[workflowkey]
@@ -635,7 +814,7 @@ const UsecaseListComponent = (props) => {
//console.log("WORKFLOW: ", newname, newusecases)
if (newusecases.includes(newname)) {
workflowBuilt = true
workflowBuilt = workflow.id
break
}
}
@@ -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) {
@@ -676,7 +855,10 @@ const UsecaseListComponent = (props) => {
showTryit={false}
shownColor={""}
workflowBuilt={workflowBuilt}
inputWorkflowId={workflowBuilt}
usecaseDetails={usecaseDetails}
isModalOpenDefault={autoOpenUsecase?.name === subcase.name}
onClose={handleUsecaseClose}
/>
</Grid>
@@ -716,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) {
@@ -728,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: {
@@ -830,6 +950,7 @@ const Usecases2 = (props) => {
})
.catch((error) => {
toast(error.toString());
setIsLoading(false);
})
}
@@ -1220,8 +1341,7 @@ const Usecases2 = (props) => {
) : null
const data =
<div className="content" style={{width: isMobile ? "100%": leftSideBarOpenByClick ? 1000: 1200, margin: "auto", paddingBottom: 200, textAlign: "center", paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease, width 0.3s ease"}}>
<div className="content" style={{width: isMobile ? "100%": leftSideBarOpenByClick ? 1000: 1200, margin: "auto", paddingBottom: 200, textAlign: "center", paddingLeft: leftSideBarOpenByClick ? 50 : 0, transition: "padding-left 0.3s ease, width 0.3s ease"}}>
<UsecaseListComponent
userdata={userdata}
isLoggedIn={isLoggedIn}
+1
View File
@@ -447,6 +447,7 @@ const Welcome = (props) => {
}
navigate("/welcome?tab=2")
setActiveStep(1)
setShowWelcome(true)
}}>
<CardActionArea style={actionObject}>
+111 -102
View File
@@ -136,7 +136,7 @@ export const GetIconInfo = (action) => {
{ key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] },
{ key: "merge", values: ["join", "merge", "route", "router"] },
{ key: "merge", values: ["join", "merge", "route", "router", "routing"] },
{
key: "search",
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
@@ -200,7 +200,7 @@ export const GetIconInfo = (action) => {
},
{
key: "compare",
values: ["compare", "convert", "to", "filter", "translate", "parse"],
values: ["compare", "convert", "to", "filter", "translate", "parse", "generate", ],
},
{ key: "close", values: ["close", "stop", "cancel", "block"] },
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
@@ -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
}
}
+68 -41
View File
@@ -92,6 +92,7 @@ import {
ArrowRight as ArrowRightIcon,
Visibility as VisibilityIcon,
EditNote as EditNoteIcon,
ErrorOutline as ErrorOutlineIcon,
} from "@mui/icons-material";
// Additional Components
@@ -108,6 +109,8 @@ import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinem
import { debounce } from "lodash";
import { removeQuery } from "../components/ScrollToTop.jsx";
import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx"
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240");
@@ -649,7 +652,6 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
const Workflows2 = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
const { leftSideBarOpenByClick } = useContext(Context);
const location = useLocation();
const navigate = useNavigate();
const [currTab, setCurrTab] = useState(0);
@@ -1115,12 +1117,9 @@ const Workflows2 = (props) => {
<Button
style={{}}
onClick={() => {
console.log("Editing: ", editingWorkflow);
if (selectedWorkflowId) {
deleteWorkflow(selectedWorkflowId)
setTimeout(() => {
getAvailableWorkflows();
}, 1000);
} else if (selectedWorkflowIndexes.length > 0) {
// Do backwards so it doesn't change
toast("Starting deletion of workflows. This might take a while.")
@@ -1131,13 +1130,13 @@ const Workflows2 = (props) => {
}
}
setTimeout(() => {
getAvailableWorkflows()
}, 1000);
setTimeout(() => {
getAvailableWorkflows()
}, 5000)
setSelectedWorkflowIndexes([]);
}
setDeleteModalOpen(false);
setDeleteModalOpen(false)
}}
color="primary"
>
@@ -1188,32 +1187,32 @@ const Workflows2 = (props) => {
"",
data.status,
)
.then((response) => {
if (response !== undefined) {
// SET THE FULL THING
data.id = response.id;
.then((response) => {
if (response !== undefined) {
// SET THE FULL THING
data.id = response.id;
// Actually create it
setNewWorkflow(
data.name,
data.description,
data.tags,
data.default_return_value,
data,
false,
[],
"",
data.status
).then((response) => {
if (response !== undefined) {
toast(`Successfully imported ${data.name}`);
}
});
}
})
.catch((error) => {
toast("Import error: " + error.toString());
});
// Actually create it
setNewWorkflow(
data.name,
data.description,
data.tags,
data.default_return_value,
data,
false,
[],
"",
data.status
).then((response) => {
if (response !== undefined) {
toast(`Successfully imported ${data.name}`);
}
});
}
})
.catch((error) => {
toast("Import error: " + error.toString());
});
});
} catch (e) {
console.log("Error in dropzone: ", e);
@@ -1936,6 +1935,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]
);
@@ -2363,7 +2366,6 @@ const Workflows2 = (props) => {
<MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => {
setDeleteModalOpen(true);
setSelectedWorkflowId(data.id);
@@ -2480,7 +2482,7 @@ const Workflows2 = (props) => {
return (
<div style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "2px solid #40E0D0" : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme?.typography?.fontFamily }}>
<div style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme?.typography?.fontFamily }}>
<Paper square style={paperAppStyle}>
{selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
@@ -2564,13 +2566,13 @@ const Workflows2 = (props) => {
{(isDistributed || hasSuborgs) && (
<div style={{
backgroundColor: "rgba(64,224,208,0.1)", // Matching the teal color used in border
border: "1px solid #40E0D0",
border: `1px solid ${theme.palette.distributionColor}`,
borderRadius: 4,
padding: "8px 12px",
marginTop: 8,
}}>
<Typography variant="body2" style={{
color: "#40E0D0",
color: theme.palette.distributionColor,
display: "flex",
alignItems: "center",
gap: 8,
@@ -2808,6 +2810,7 @@ const Workflows2 = (props) => {
{workflowMenuButtons}
</div>
) : null}
{(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, }}>
@@ -2822,10 +2825,33 @@ const Workflows2 = (props) => {
>
<EditNoteIcon />
</IconButton>
{workflowMenuButtons}
</div>
</Tooltip>
: null}
: 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, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={() => {
window.open(`/admin?admin_tab=notifications&workflow=${data.id}`, "_blank")
}}
style={{
padding: "0px",
color: "#979797",
}}
>
<ErrorOutlineIcon style={{
marginRight: 2,
}} />
</IconButton>
</div>
</Tooltip>
: null}
</Grid>
</Paper>
</div>
@@ -2852,6 +2878,7 @@ const Workflows2 = (props) => {
if (editingWorkflow.id !== undefined) {
console.log("Building original workflow");
method = "PUT";
//extraData = "/" + editingWorkflow.id + "?skip_save=true";
extraData = "/" + editingWorkflow.id + "?skip_save=true";
workflowdata = editingWorkflow;
@@ -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 }}
@@ -0,0 +1,30 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "shuffle.backend.name" . }}-env
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
data:
BACKEND_PORT: "5001"
{{- if .Values.shuffle.baseUrl }}
BASE_URL: "{{ .Values.shuffle.baseUrl }}"
SSO_REDIRECT_URL: "{{ .Values.shuffle.baseUrl }}"
{{- else }}
BASE_URL: "http://{{ include "shuffle.backend.name" . }}:5001"
{{- end }}
ORG_ID: "{{ .Values.shuffle.org }}"
SHUFFLE_APP_DOWNLOAD_LOCATION: "{{ .Values.backend.apps.downloadLocation }}"
SHUFFLE_DOWNLOAD_AUTH_BRANCH: "{{ .Values.backend.apps.downloadBranch }}"
SHUFFLE_APP_FORCE_UPDATE: "{{ .Values.backend.apps.forceUpdate }}"
SHUFFLE_CHAT_DISABLED: "true"
SHUFFLE_OPENSEARCH_URL: {{ include "common.tplvalues.render" (dict "value" .Values.backend.openSearch.url "context" $) }}
SHUFFLE_OPENSEARCH_USERNAME: "{{ .Values.backend.openSearch.username }}"
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE: "{{ .Values.backend.openSearch.certificateFile }}"
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY: "{{ .Values.backend.openSearch.skipSSLVerify }}"
SHUFFLE_OPENSEARCH_INDEX_PREFIX: "{{ .Values.backend.openSearch.indexPrefix }}"
SHUFFLE_RERUN_SCHEDULE: "{{ .Values.backend.cleanupSchedule }}"
TZ: "{{ .Values.shuffle.timezone }}"
REGISTRY_URL: "{{ .Values.shuffle.appRegistry }}"
@@ -0,0 +1,224 @@
apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }}
kind: Deployment
metadata:
name: {{ template "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if or .Values.backend.deploymentAnnotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.backend.deploymentAnnotations .Values.commonAnnotations) "context" .) }}
annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.backend.autoscaling.hpa.enabled }}
replicas: {{ .Values.backend.replicaCount }}
{{- end }}
{{- if .Values.backend.updateStrategy }}
strategy: {{- toYaml .Values.backend.updateStrategy | nindent 4 }}
{{- end }}
{{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }}
selector:
matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }}
template:
metadata:
{{- if .Values.backend.podAnnotations }}
annotations: {{- include "common.tplvalues.render" (dict "value" .Values.backend.podAnnotations "context" $) | nindent 8 }}
{{- end }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" $podLabels "context" $ ) | nindent 8 }}
spec:
{{- include "shuffle.backend.imagePullSecrets" . | nindent 6 }}
serviceAccountName: {{ template "shuffle.backend.serviceAccount.name" . }}
automountServiceAccountToken: {{ .Values.backend.automountServiceAccountToken }}
{{- if .Values.backend.hostAliases }}
hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.backend.hostAliases "context" $) | nindent 8 }}
{{- end }}
{{- if .Values.backend.affinity }}
affinity: {{- include "common.tplvalues.render" ( dict "value" .Values.backend.affinity "context" $) | nindent 8 }}
{{- else }}
affinity:
podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.backend.podAffinityPreset "component" "backend" "customLabels" $podLabels "context" $) | nindent 10 }}
podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.backend.podAntiAffinityPreset "component" "backend" "customLabels" $podLabels "context" $) | nindent 10 }}
nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.backend.nodeAffinityPreset.type "key" .Values.backend.nodeAffinityPreset.key "values" .Values.backend.nodeAffinityPreset.values) | nindent 10 }}
{{- end }}
{{- if .Values.backend.nodeSelector }}
nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.backend.nodeSelector "context" $) | nindent 8 }}
{{- end }}
{{- if .Values.backend.tolerations }}
tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.backend.tolerations "context" .) | nindent 8 }}
{{- end }}
{{- if .Values.backend.priorityClassName }}
priorityClassName: {{ .Values.backend.priorityClassName | quote }}
{{- end }}
{{- if .Values.backend.schedulerName }}
schedulerName: {{ .Values.backend.schedulerName | quote }}
{{- end }}
{{- if .Values.backend.topologySpreadConstraints }}
topologySpreadConstraints: {{- include "common.tplvalues.render" (dict "value" .Values.backend.topologySpreadConstraints "context" .) | nindent 8 }}
{{- end }}
{{- if .Values.backend.podSecurityContext.enabled }}
securityContext: {{- omit .Values.backend.podSecurityContext "enabled" | toYaml | nindent 8 }}
{{- end }}
{{- if .Values.backend.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.backend.terminationGracePeriodSeconds }}
{{- end }}
initContainers:
{{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }}
- name: volume-permissions
image: {{ include "shuffle.volumePermissions.image" . }}
imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }}
command:
- /bin/bash
- -ec
- |
chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /app/generated && \
chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /shuffle-apps && \
chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /shuffle-files
{{- if .Values.volumePermissions.containerSecurityContext.enabled }}
securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.volumePermissions.containerSecurityContext "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.volumePermissions.resources }}
resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }}
{{- else if ne .Values.volumePermissions.resourcesPreset "none" }}
resources: {{- include "common.resources.preset" (dict "type" .Values.volumePermissions.resourcesPreset) | nindent 12 }}
{{- end }}
volumeMounts:
- name: shuffle-app-builder
mountPath: /app/generated
- name: shuffle-apps
mountPath: /shuffle-apps
{{- if .Values.persistence.apps.subPath }}
subPath: {{ .Values.persistence.apps.subPath }}
{{- end }}
- name: shuffle-files
mountPath: /shuffle-files
{{- if .Values.persistence.files.subPath }}
subPath: {{ .Values.persistence.apps.subPath }}
{{- end }}
{{- end }}
{{- if .Values.backend.initContainers }}
{{- include "common.tplvalues.render" (dict "value" .Values.backend.initContainers "context" $) | nindent 8 }}
{{- end }}
containers:
- name: backend
image: {{ template "shuffle.backend.image" . }}
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
{{- if .Values.backend.containerSecurityContext.enabled }}
securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.backend.containerSecurityContext "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.diagnosticMode.enabled }}
command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }}
{{- else if .Values.backend.command }}
command: {{- include "common.tplvalues.render" (dict "value" .Values.backend.command "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.diagnosticMode.enabled }}
args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }}
{{- else if .Values.backend.args }}
args: {{- include "common.tplvalues.render" (dict "value" .Values.backend.args "context" $) | nindent 12 }}
{{- end }}
env:
- name: RUNNING_MODE
value: kubernetes
- name: IS_KUBERNETES
value: "true"
- name: SHUFFLE_APP_HOTLOAD_FOLDER
value: /shuffle-apps
- name: SHUFFLE_FILE_LOCATION
value: /shuffle-files
{{- if .Values.backend.extraEnvVars }}
{{- include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVars "context" $) | nindent 12 }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "shuffle.backend.name" . }}-env
{{- if .Values.backend.extraEnvVarsCM }}
- configMapRef:
name: {{ include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVarsCM "context" $) }}
{{- end }}
{{- if .Values.backend.extraEnvVarsSecret }}
- secretRef:
name: {{ include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVarsSecret "context" $) }}
{{- end }}
{{- if .Values.backend.resources }}
resources: {{- toYaml .Values.backend.resources | nindent 12 }}
{{- else if ne .Values.backend.resourcesPreset "none" }}
resources: {{- include "common.resources.preset" (dict "type" .Values.backend.resourcesPreset) | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.backend.containerPorts.http }}
{{- if .Values.backend.extraContainerPorts }}
{{- include "common.tplvalues.render" (dict "value" .Values.backend.extraContainerPorts "context" $) | nindent 12 }}
{{- end }}
{{- if not .Values.diagnosticMode.enabled }}
{{- if .Values.backend.customLivenessProbe }}
livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customLivenessProbe "context" $) | nindent 12 }}
{{- else if .Values.backend.livenessProbe.enabled }}
livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.livenessProbe "enabled") "context" $) | nindent 12 }}
httpGet:
path: /api/v1/health
port: {{ .Values.backend.containerPorts.http }}
{{- end }}
{{- if .Values.backend.customReadinessProbe }}
readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customReadinessProbe "context" $) | nindent 12 }}
{{- else if .Values.backend.readinessProbe.enabled }}
readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.readinessProbe "enabled") "context" $) | nindent 12 }}
httpGet:
path: /api/v1/health
port: {{ .Values.backend.containerPorts.http }}
{{- end }}
{{- if .Values.backend.customStartupProbe }}
startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customStartupProbe "context" $) | nindent 12 }}
{{- else if .Values.backend.startupProbe.enabled }}
startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.startupProbe "enabled") "context" $) | nindent 12 }}
httpGet:
path: /api/v1/health
port: {{ .Values.backend.containerPorts.http }}
{{- end }}
{{- end }}
{{- if .Values.backend.lifecycleHooks }}
lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.backend.lifecycleHooks "context" $) | nindent 12 }}
{{- end }}
volumeMounts:
- name: shuffle-app-builder
mountPath: /app/generated
- name: shuffle-apps
mountPath: /shuffle-apps
{{- if .Values.persistence.apps.subPath }}
subPath: {{ .Values.persistence.apps.subPath }}
{{- end }}
- name: shuffle-files
mountPath: /shuffle-files
{{- if .Values.persistence.files.subPath }}
subPath: {{ .Values.persistence.apps.subPath }}
{{- end }}
- name: empty-dir
mountPath: /tmp
subPath: tmp-dir
{{- if .Values.backend.extraVolumeMounts }}
{{- include "common.tplvalues.render" (dict "value" .Values.backend.extraVolumeMounts "context" $) | nindent 12 }}
{{- end }}
{{- if .Values.backend.sidecars }}
{{- include "common.tplvalues.render" ( dict "value" .Values.backend.sidecars "context" $) | nindent 8 }}
{{- end }}
volumes:
- name: empty-dir
emptyDir: {}
- name: shuffle-app-builder
persistentVolumeClaim:
claimName: backend-apps-claim
- name: shuffle-apps
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ default (printf "%s-apps" (include "shuffle.backend.name" .)) .Values.persistence.apps.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
- name: shuffle-files
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ default (printf "%s-files" (include "shuffle.backend.name" .)) .Values.persistence.apps.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
{{- if .Values.backend.extraVolumes }}
{{- include "common.tplvalues.render" (dict "value" .Values.backend.extraVolumes "context" $) | nindent 8 }}
{{- end }}
@@ -0,0 +1,30 @@
{{- if .Values.persistence.enabled }}
{{- if (not .Values.persistence.files.existingClaim) }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ printf "%s-files" (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.files.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.files.annotations .Values.commonAnnotations ) "context" . ) }}
{{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- range .Values.persistence.files.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.files.size }}
{{- if .Values.persistence.files.selector }}
selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.files.selector "context" $) | nindent 2 }}
{{- end }}
{{- include "common.storage.class" ( dict "persistence" .Values.persistence.files "global" .Values.global ) | nindent 2 }}
{{- end }}
{{- end }}
@@ -0,0 +1,43 @@
{{- if .Values.backend.autoscaling.hpa.enabled }}
apiVersion: {{ include "common.capabilities.hpa.apiVersion" . }}
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }}
kind: Deployment
name: {{ include "shuffle.backend.name" . }}
minReplicas: {{ .Values.backend.autoscaling.hpa.minReplicas }}
maxReplicas: {{ .Values.backend.autoscaling.hpa.maxReplicas }}
metrics:
{{- if .Values.backend.autoscaling.hpa.targetMemory }}
- type: Resource
resource:
name: memory
{{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }}
targetAverageUtilization: {{ .Values.backend.autoscaling.hpa.targetMemory }}
{{- else }}
target:
type: Utilization
averageUtilization: {{ .Values.worker.autoscaling.hpa.targetMemory }}
{{- end }}
{{- end }}
{{- if .Values.backend.autoscaling.hpa.targetCPU }}
- type: Resource
resource:
name: cpu
{{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }}
targetAverageUtilization: {{ .Values.backend.autoscaling.hpa.targetCPU }}
{{- else }}
target:
type: Utilization
averageUtilization: {{ .Values.worker.autoscaling.hpa.targetCPU }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,66 @@
{{- if .Values.backend.networkPolicy.enabled }}
kind: NetworkPolicy
apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }}
metadata:
name: {{ template "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
{{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.podLabels .Values.commonLabels ) "context" . ) }}
podSelector:
matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }}
policyTypes:
- Ingress
- Egress
egress:
{{- if .Values.backend.networkPolicy.allowExternalEgress }}
- {}
{{- else }}
# Allow DNS resolution with an in-cluster DNS server
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
{{- if .Values.backend.networkPolicy.extraEgress }}
{{- include "common.tplvalues.render" ( dict "value" .Values.backend.networkPolicy.extraEgress "context" $ ) | nindent 4 }}
{{- end }}
{{- end }}
ingress:
- ports:
- port: {{ .Values.backend.containerPorts.http }}
protocol: TCP
{{- if not .Values.backend.networkPolicy.allowExternal }}
from:
# Allow traffic from orborus
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Release.Namespace }}
podSelector:
matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }}
# Allow traffic from workers
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Release.Namespace }}
podSelector:
matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }}
# Allow traffic from apps
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Release.Namespace }}
podSelector:
matchLabels: {{ include "shuffle.app.matchLabels" . | nindent 14 }}
{{- end }}
{{- if .Values.backend.networkPolicy.extraIngress }}
{{- include "common.tplvalues.render" ( dict "value" .Values.backend.networkPolicy.extraIngress "context" $ ) | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,21 @@
{{- if .Values.backend.pdb.create }}
apiVersion: {{ include "common.capabilities.policy.apiVersion" . }}
kind: PodDisruptionBudget
metadata:
name: {{ include "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
{{- if .Values.backend.pdb.minAvailable }}
minAvailable: {{ .Values.backend.pdb.minAvailable }}
{{- end }}
{{- if or .Values.backend.pdb.maxUnavailable ( not .Values.backend.pdb.minAvailable ) }}
maxUnavailable: {{ .Values.backend.pdb.maxUnavailable | default 1 }}
{{- end }}
{{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.podLabels .Values.commonLabels ) "context" . ) }}
selector:
matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }}
{{- end }}
@@ -0,0 +1,18 @@
{{ if .Values.backend.rbac.create }}
kind: RoleBinding
apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }}
metadata:
name: {{ include "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
subjects:
- kind: ServiceAccount
name: {{ include "shuffle.backend.serviceAccount.name" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "shuffle.backend.name" . }}
{{- end }}
@@ -0,0 +1,18 @@
{{ if .Values.backend.rbac.create }}
kind: Role
apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }}
metadata:
name: {{ include "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "create", "delete"]
{{- end }}
@@ -0,0 +1,14 @@
{{- if .Values.backend.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "shuffle.backend.serviceAccount.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if or .Values.backend.serviceAccount.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.backend.serviceAccount.annotations .Values.commonAnnotations) "context" .) }}
annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.backend.serviceAccount.automountServiceAccountToken }}
{{- include "shuffle.backend.serviceAccount.imagePullSecrets" . | nindent 0 }}
{{- end }}
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" (dict "customLabels" .Values.commonLabels "context" $) | nindent 4 }}
{{- if .Values.commonAnnotations }}
annotations: {{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }}
{{- end }}
spec:
type: ClusterIP
ports:
- name: http
port: {{ .Values.backend.containerPorts.http }}
targetPort: http
protocol: TCP
{{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }}
selector: {{- include "shuffle.backend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }}
@@ -0,0 +1,38 @@
{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1/VerticalPodAutoscaler") .Values.backend.autoscaling.vpa.enabled }}
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ include "shuffle.backend.name" . }}
namespace: {{ include "common.names.namespace" . | quote }}
labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- if or .Values.backend.autoscaling.vpa.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.autoscaling.vpa.annotations .Values.commonAnnotations ) "context" . ) }}
annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }}
{{- end }}
spec:
resourcePolicy:
containerPolicies:
- containerName: backend
{{- with .Values.backend.autoscaling.vpa.controlledResources }}
controlledResources:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.backend.autoscaling.vpa.maxAllowed }}
maxAllowed:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.backend.autoscaling.vpa.minAllowed }}
minAllowed:
{{- toYaml . | nindent 8 }}
{{- end }}
targetRef:
apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }}
kind: Deployment
name: {{ include "backend.names.name" . }}
{{- if .Values.backend.autoscaling.vpa.updatePolicy }}
updatePolicy:
{{- with .Values.backend.autoscaling.vpa.updatePolicy.updateMode }}
updateMode: {{ . }}
{{- end }}
{{- end }}
{{- end }}

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