diff --git a/.github/workflows/dockerbuild.yaml b/.github/workflows/dockerbuild.yaml index 85890765..a04e87bd 100644 --- a/.github/workflows/dockerbuild.yaml +++ b/.github/workflows/dockerbuild.yaml @@ -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 diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 00000000..e365080e --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -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 diff --git a/.github/workflows/quick-testing.yml b/.github/workflows/quick-testing.yml index 17ac28ac..6bf468d0 100644 --- a/.github/workflows/quick-testing.yml +++ b/.github/workflows/quick-testing.yml @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile index c591da21..7cab5e27 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index a6dfc67d..57d4aaf8 100755 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -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 { diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index a3b1a914..f9094264 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -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 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 48beb8f8..5135bcff 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -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= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 53d0f498..dfdce781 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -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") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index d6cac1fe..df7addb8 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -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) } diff --git a/docker-compose.yml b/docker-compose.yml index 8459fc88..84b1f321 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 807ca56d..aa5a0992 100755 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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 diff --git a/frontend/public/images/frameworks/attack.png b/frontend/public/images/frameworks/attack.png new file mode 100644 index 00000000..80f2dbb2 Binary files /dev/null and b/frontend/public/images/frameworks/attack.png differ diff --git a/frontend/public/images/frameworks/openapi.png b/frontend/public/images/frameworks/openapi.png new file mode 100644 index 00000000..bf3fc39b Binary files /dev/null and b/frontend/public/images/frameworks/openapi.png differ diff --git a/frontend/public/images/frameworks/python.jpeg b/frontend/public/images/frameworks/python.jpeg new file mode 100644 index 00000000..b72240b6 Binary files /dev/null and b/frontend/public/images/frameworks/python.jpeg differ diff --git a/frontend/public/images/frameworks/resized/attack.png b/frontend/public/images/frameworks/resized/attack.png new file mode 100644 index 00000000..8bbbe126 Binary files /dev/null and b/frontend/public/images/frameworks/resized/attack.png differ diff --git a/frontend/public/images/frameworks/resized/openapi.png b/frontend/public/images/frameworks/resized/openapi.png new file mode 100644 index 00000000..31dba79d Binary files /dev/null and b/frontend/public/images/frameworks/resized/openapi.png differ diff --git a/frontend/public/images/frameworks/resized/sigma.png b/frontend/public/images/frameworks/resized/sigma.png new file mode 100644 index 00000000..2a1c7c40 Binary files /dev/null and b/frontend/public/images/frameworks/resized/sigma.png differ diff --git a/frontend/public/images/frameworks/sigma.png b/frontend/public/images/frameworks/sigma.png new file mode 100644 index 00000000..0bd0db14 Binary files /dev/null and b/frontend/public/images/frameworks/sigma.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8f7ec2e1..34a952e6 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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") ? -
- : - isLoggedIn ? -
- + { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? ( +
+
- : -
-
-
- } + ) : ( +
+ +
+ ) } {/*
@@ -262,7 +254,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) => { /> } /> - - } - /> + + } + /> + + } + /> + + } + /> + + } + /> { + 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: , alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } }, + { iconSrc: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } }, + { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, + { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} }, + { iconSrc: , alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } }, + { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } }, + { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, + { iconSrc: , alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } } + ]; + + 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 ; + }; + + const ComponentToRender = selectedItemData.component; + const componentProps = selectedItemData.props; + + return ; + }; + + const defaultImage = "/images/logos/orange_logo.svg" + const imageData = + selectedOrganization?.image === undefined || selectedOrganization?.image.length === 0 + ? defaultImage + : selectedOrganization?.image; + + return ( + +
+ +
+ {renderComponent()} +
+ ); +}; + +export default AdminNavBar; + +const PaddingWrapper2 = memo(({ children }) => { + + return ( +
+ {children} +
+ ) +}); + +const Wrapper2 = memo(({children}) => { + + return ( + + {children} + + ); +}) + +const PaddingWrapper = memo(({ children }) => { + const { leftSideBarOpenByClick, windowWidth } = useContext(Context); + + return ( +
+ {children} +
+ ); +}); + +const Wrapper = memo(({ children }) => { + return ( + + {children} + + ); +}) diff --git a/frontend/src/components/AnalyticsTab.jsx b/frontend/src/components/AnalyticsTab.jsx new file mode 100644 index 00000000..8938262e --- /dev/null +++ b/frontend/src/components/AnalyticsTab.jsx @@ -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 ( +
+
+
Timeline
+
+
+
+
+
+
Apps
+ Category +
+ { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand +
+ {expand ? null : +
+
+ Onboarding + + +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+
+
+
+
+ Other + + +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+
+
+
} +
+
+
Workflows
+
+ + + +
+ +
+
+
+
Insights
+
+
+
+
Sessions Overview
+
+
+
+ 2.8 Hours +
+
+ Avg. Activity per session +
+
+
+
+ /usercases/edr to ticket +
+
+ Last visited page +
+
+
+
+ /workflow/email management +
+
+ Most visited page +
+
+
+
+
+ ); +}; + +export default AnalyticsTab; diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index c2dfaeb3..e6080301 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -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 (
{info?.title ? ( -
- app logo - - {info.title} - -
+ + +
+ app logo + + {info.title} + +
+
+
) : ( @@ -1463,9 +1478,10 @@ const ActionsList = memo(({ )}
+ +
- {action.name.replaceAll("_", " ")} + {action?.name?.replaceAll("_", " ")} )) @@ -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 (
- {action.name} + {actionname}
{ - 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; diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx new file mode 100644 index 00000000..43fa1c0f --- /dev/null +++ b/frontend/src/components/AppAuthTab.jsx @@ -0,0 +1,2656 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import { + Edit as EditIcon, + SelectAll as SelectAllIcon, + Delete as DeleteIcon, + CheckCircle as CheckCircleIcon, + Cancel as CancelIcon, + Search as SearchIcon, + Clear as ClearIcon, + DragIndicator as DragIndicatorIcon, + Close as CloseIcon, + LockOpen as LockOpenIcon, +} from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import theme from "../theme.jsx"; +import Markdown from "react-markdown"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import { isMobile } from "react-device-detect" +import PaperComponent from "../components/PaperComponent.jsx"; +import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' +import { v4 as uuidv4} from "uuid"; + +import { + Divider, + List, + ListItem, + ListItemText, + IconButton, + Tooltip, + Chip, + Checkbox, + Typography, + TextField, + Button, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Select, + FormControl, + InputLabel, + MenuItem, + FormControlLabel, + InputAdornment, + Grid, + Zoom, + Paper, + Skeleton, + Link, +} from "@mui/material"; + +import algoliasearch from "algoliasearch/lite"; +import { + InstantSearch, + Configure, + connectSearchBox, + connectHits, + connectHitInsights, + RefinementList, + ClearRefinements, + connectStateResults +} from "react-instantsearch-dom"; +import aa from "search-insights"; +import { Context } from '../context/ContextApi.jsx'; + +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +) + +const AppAuthTab = memo((props) => { + const { globalUrl, userdata, isCloud, selectedOrganization } = props; + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}); + const [authentication, setAuthentication] = React.useState([]); + const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); + const [authenticationFields, setAuthenticationFields] = React.useState([]); + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState(""); + const [environments, setEnvironments] = React.useState([]); + const [listItemExpanded, setListItemExpanded] = React.useState(-1); + const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false); + const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); + const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); + const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); + const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); + const [searchQuery, setSearchQuery] = React.useState(""); + const [showAppModal, setShowAppModal] = useState(false) + const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) + const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) + const changeDistribution = (data) => { + //changeDistributed(data, !isDistributed) + editAuthenticationConfig(data.id, "suborg_distribute") + } + + useEffect(() => { + getAppAuthentication(); + getAppAuthenticationGroups(); + getEnvironments(); + }, []) + + const getAppAuthentication = () => { + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAuthentication(responseJson.data); + setShowAuthenticationLoader(false) + } else { + toast("Failed getting authentications"); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true); + setSelectedAuthentication(field); + //{selectedAuthentication.fields.map((data, index) => { + var newfields = []; + for (var key in field.fields) { + newfields.push({ + key: field.fields[key].key, + value: "", + }); + } + setAuthenticationFields(newfields); + }; + const saveAuthentication = (authentication) => { + const data = authentication; + const url = globalUrl + "/api/v1/apps/authentication"; + + fetch(url, { + mode: "cors", + method: "PUT", + 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) { + // Check if .reason exists + if (responseJson.reason !== undefined) { + toast("Failed changing authentication: " + responseJson.reason); + } else { + toast("Failed changing authentication"); + } + } else { + getAppAuthentication(); + + + setSelectedAuthentication({}); + setSelectedAuthenticationModalOpen(false); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const deleteAuthentication = (data) => { + toast("Deleting auth " + data.label); + + // Just use this one? + const url = globalUrl + "/api/v1/apps/authentication/" + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson.success === false) { + toast("Failed deleting auth"); + } else { + // Need to wait because query in ES is too fast + setTimeout(() => { + getAppAuthentication(); + }, 1000); + //toast("Successfully deleted authentication!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const editAuthenticationConfig = (id, parentAction) => { + const data = { + id: id, + action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", + } + + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/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 appauth"); + } else { + toast("Successfully updated auth!"); + setSelectedUserModalOpen(false); + setTimeout(() => { + getAppAuthentication(); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const editAuthenticationModal = selectedAuthenticationModalOpen ? ( + { + setSelectedAuthenticationModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + 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, + }, + }, + }} + > + + + Edit authentication for {selectedAuthentication.app.name.replaceAll("_", " ")} ( + {selectedAuthentication.label}) + + + You can not see the previous values for an authentication while editing. This is to keep your data secure. You can overwrite one- or multiple fields at a time. + + + + + Authentication Label + + { + selectedAuthentication.label = e.target.value + }} + /> + + + {selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app" ? +
+ + Only the name and url can be modified for Oauth2/OpenID connect. Please remake the authentication if you want to change the other fields like Client ID, Secret, Scopes etc. + +
+ : null} + + {selectedAuthentication.fields.map((data, index) => { + var fieldname = data.key.replaceAll("_", " ") + if (fieldname.endsWith(" basic")) { + fieldname = fieldname.substring(0, fieldname.length - 6) + } + + if (selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app") { + if (selectedAuthentication.fields[index].key !== "url") { + return null + } + } + + + //console.log("DATA: ", data, selectedAuthentication) + return ( +
+ + {fieldname} + + { + authenticationFields[index].value = e.target.value; + setAuthenticationFields(authenticationFields); + }} + /> +
+ ); + })} +
+ + + + +
+ ) : null; + + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + setEnvironments(responseJson); + + // Helper info for users in case they have a large queue and don't know about queue flushing + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + if (responseJson.length === 1 && responseJson[0].Type !== "cloud") { + setListItemExpanded(0) + } + + for (var i = 0; i < responseJson.length; i++) { + const env = responseJson[i]; + + // Check if queuesize is too large + if ( + env.queue !== undefined && + env.queue !== null && + env.queue > 100 + ) { + toast( + "Queue size for " + + env.name + + " is very large. We recommend you to reduce it by flushing the queue before continuing.", + ); + break; + } + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const getAppAuthenticationGroups = () => { + //console.log("DEBUG: Skipping app auth group loading") + //return + + fetch(globalUrl + "/api/v1/authentication/group", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAppAuthenticationGroups(responseJson.data); + setShowAppAuthGroupLoader(false) + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const deleteAppAuthenticationGroup = (appAuthGroupId) => { + const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}` + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for deleting app auth group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to delete app authentication group"); + } else { + toast("App authentication group deleted") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()) + }) + } + + const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => { + // Makes list of ids into a full-on list of auth, but just with the ID + // The backend fills in the rest + console.log("INput auth: ", appAuthIds) + let app_auths = appAuthIds.map((appAuthId) => { + return { id: appAuthId }; + }) + + var parsedAppGroup = { + label: name, + environment: environment, + description: description, + app_auths: app_auths + } + + if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") { + parsedAppGroup.id = appAuthenticationGroupId + } + + fetch(globalUrl + "/api/v1/authentication/group", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(parsedAppGroup), + }) + .then((response) => { + if (response.status !== 200) { + throw new Error("Failed to create app authentication group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to create. Please try again, or contact support@shuffler.io") + } else { + // Close the modal + setAppAuthenticationGroupModalOpen(false) + + toast("App authentication group created") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleAppAuthGroupCheckbox = (data) => { + //let groupApp = data.app.id + var newappauth = appsForAppAuthGroup + if (appsForAppAuthGroup.includes(data.app.id)) { + newappauth = newappauth.filter((item) => item !== data.app.id) + } + + if (appsForAppAuthGroup.includes(data.id)) { + // Remove app from app auth group + newappauth = newappauth.filter((item) => item !== data.id) + setAppsForAppAuthGroup(newappauth) + return + } + + for (var i = 0; i < authentication.length; i++) { + if (authentication[i].id === data.id) { + continue + } + + if (!appsForAppAuthGroup.includes(authentication[i].id)) { + continue + } + + if (authentication[i].app.id === data.app.id) { + // Remove app from app auth group + newappauth = newappauth.filter((item) => item !== authentication[i].id) + toast(`App ${data.app.name} is already in this group`) + } + } + + setAppsForAppAuthGroup(newappauth.concat(data.id)) + } + + const authenticationView = appAuthenticationGroupModalOpen ? + ( +
+ {/* (appAuthenticationGroupModalOpen : { */} + {appAuthenticationGroupModalOpen && ( + { + setAppAuthenticationGroupModalOpen(false); + + setAppAuthenticationGroupId("") + setAppAuthenticationGroupName("") + setAppAuthenticationGroupEnvironment("") + setAppAuthenticationGroupDescription("") + setAppsForAppAuthGroup([]) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "1000px", + padding: "25px", + paddingLeft: "50px", + minHeight: "320px", + 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, + }, + }, + }} + sx={{ + "& .MuiDialog-paper": { + backgroundColor: "rgb(26, 26, 26)", + }, + }} + > + + App Authentication Groups + + + +
+
+ + Name + + { + setAppAuthenticationGroupName(event.target.value); + }} + /> +
+
+ + Evironment + + {environments !== undefined && environments !== null && environments.length > 0 ? + + : + + Locations failed to load. Please try again + + } +
+
+ +
+
+ + + +
+ {/* Show a check box list of all app authentications to add to the auth group */} +
+ {authentication.map((data, index) => { + var checked = data.checked + if (data.label !== undefined && data.label !== null && data.label.toLowerCase() === "kms shuffle storage") { + return null + } + + if (checked === undefined || checked === null) { + checked = false + } + + if (appsForAppAuthGroup.includes(data.id)) { + checked = true + } + + return ( +
+ +
+ + { + handleAppAuthGroupCheckbox(data) + }} + name={data.label} + disabled={data.app.id in appsForAppAuthGroup} + /> +
+ + } + label={data.label} + /> +
+ ) + })} +
+
+ + +
+
+ )} + + +
+ ): null + + + + const appModal = showAppModal ? ( + { + setShowAppModal(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + width: '700px', + maxWidth: '700px', + overflowY: 'hidden', + height: "600px", + maxHeight: "600px", + fontFamily: theme?.typography?.fontFamily, + zIndex: 1000, + '& .MuiDialogContent-root': { + padding: '30px', + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > +
+ + + Add App Authentication + + + + + + +
+
+ ) : null; + + return ( +
+ {appModal} +
+
+
+

App Authentication

+
+ + Control the authentication options for individual apps. + +   + + Learn more about App Authentication + +
+
+ +
+ {/* */} + +
+ + + + + + {["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => ( + + ))} + + + {showAuthenticationLoader + ? + [...Array(6)].map((_, rowIndex) => ( + + {Array(8) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + : authentication?.length === 0 ? ( +
+ + No authentication found. + +
+ ):authentication.map((data, index) => { + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + //console.log("Auth data: ", data) + if (data.type === "oauth2") { + data.fields = [ + { + key: "url", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_id", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_secret", + value: "Secret. Replaced during app execution!", + }, + { + key: "scope", + value: "Secret. Replaced during app execution!", + }, + ]; + } + + const isDistributed = data.suborg_distributed === true ? true : false; + var validIcon = + if (data.validation !== null && data.validation !== undefined && data.validation.valid === false) { + + if (data.validation.changed_at === 0) { + validIcon = "" + } else { + validIcon = + } + } + return ( + + + + {validIcon} + + + )} + style={{ display: "table-cell", verticalAlign: 'middle', minWidth: 60 }} + primaryTypographyProps={{ + style: { + padding: "8px 8px 8px 15px", + } + }} + onClick={() => { + if (data.validation === null || data.validation === undefined) { + return + } + + if (data.validation.workflow_id === undefined || data.validation.workflow_id === null || data.validation.workflow_id.length === 0) { + toast.warn("No workflow runs found for this auth yet. Check back later.") + return + } + + const url = `/workflows/${data.validation.workflow_id}?execution_id=${data.validation.execution_id}&node=${data.validation.node_id}` + window.open(url, "_blank") + }} + /> + + + + {data?.app?.name?.replaceAll("_", " ")} + + + } + primaryTypographyProps={{ + style: { + padding: 8 + } + }} + style={{ marginLeft: 10, display: "table-cell", textAlign: 'center', verticalAlign: 'middle', padding: 8 }} + /> + + { + return data.key; + }) + .join(", ") + } + primaryTypographyProps={{ + style: { + padding: 8 + } + }} + style={{ + overflow: "auto", + display: "table-cell", + verticalAlign: 'middle' + }} + /> + + + { + updateAppAuthentication(data); + }} + disabled={data.org_id !== selectedOrganization.id} + > + Edit icon + + {data.defined ? ( + + { + editAuthenticationConfig(data.id); + }} + > + + + + ) : ( + + {}} + disabled={data.org_id !== selectedOrganization.id} + > + + + + )} + { + deleteAuthentication(data); + }} + > + delete icon + + + + {selectedOrganization.id !== undefined && data.org_id !== selectedOrganization.id ? + + + + : + + { + changeDistribution(data, !isDistributed) + }} + /> + + } + + + ); + })} +
+
+ {editAuthenticationModal} + {authenticationView} +
+
+

App Authentication Groups

+ + Disabled until further notice. Makes a workflow run replicate across all relevant authentications in an app auth group. Useful when the EXACT same workflow is supposed to run many times from one single input. {" "} + + Learn more about App Authentication Groups + + + +
+ +
+ + + {["Label", "Environment", "App Auth", "Created At", "Actions"].map((header, index) => ( + + ))} + + {showAppAuthGroupLoader ? + [...Array(6)].map((_, rowIndex) => ( + + {Array(5) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + : appAuthenticationGroups.length === 0 ? ( +
+ + No authentication groups found. + +
+ ): appAuthenticationGroups.map((data, index) => { + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + if (data.app_auths === undefined || data.app_auths === null) { + data.app_auths = [] + } + + return ( + + + + + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })} +
+ } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + { + setAppAuthenticationGroupId(data.id) + + setAppAuthenticationGroupName(data.label) + setAppAuthenticationGroupDescription(data.description) + + setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id)) + setAppAuthenticationGroupEnvironment(data.environment) + setAppAuthenticationGroupModalOpen(true) + }} + > + edit icon + + { + deleteAppAuthenticationGroup(data.id) + }} + > + delete icon + +
+ } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + ); + } + )} + +
+ +
+
+
+
+ ); +}); + +export default AppAuthTab; + + +const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => { + const handleSearch = (e) => { + refine(searchQuery.trim()); + }; + + return ( +
+ + + + ), + endAdornment: ( + + {searchQuery?.length > 0 && ( + { + setSearchQuery('') + // removeQuery("q"); + refine('') + }} + /> + )} + + + ), + + }} + autoComplete="off" + color="primary" + placeholder="Search more than 2500 Apps" + id="shuffle_search_field" + onChange={(event) => { + setSearchQuery(event.currentTarget.value); + // removeQuery("q"); + refine(event.currentTarget.value); + }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ); +}; + +const Hits = ({ + hits, + insights, + setIsAnyAppActivated, + searchQuery, + isCloud, + globalUrl, + userdata, + getAppAuthentication +}) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + const [selectedAppData, setSelectedAppData] = useState({}) + const [appid, setAppId] = useState("") + const [selectedAuthentication, setSelectedAuthentication] = useState({}) + const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false) + const [authenticationType, setAuthenticationType] = React.useState(""); + const [appAuthentication, setAppAuthentication] = useState([]); + const [selectedMeta, setSelectedMeta] = useState(undefined); + const [selectedAction, setSelectedAction] = useState( + { + "app_name": selectedAppData.name, + "app_id": selectedAppData.id, + "app_version": selectedAppData.version, + "large_image": selectedAppData.large_image, + } + ) + const navigate = useNavigate(); + + const normalizedString = (name) => { + if (typeof name === 'string') { + return name.replace(/_/g, ' '); + } else { + return name; + } + }; + + + let workflowDelay = 0; + const isHeader = true; + const paperStyle = { + color: "rgba(241, 241, 241, 1)", + padding: isHeader ? null : 15, + cursor: "pointer", + width: '100%', + maxHeight: 96, + borderRadius: 4, + transition: 'background-color 0.3s ease', + }; + + const base64_decode = (str) => { + return decodeURIComponent( + atob(str) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); + }; + + const handleAppAuthenticationType = (selectedAppData)=> { + + if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) { + setAuthenticationType({ + type: "", + }) + + selectedAppData.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? { + type: selectedAppData.authentication.type, + redirect_uri: selectedAppData.authentication.redirect_uri, + refresh_uri: selectedAppData.authentication.refresh_uri, + token_uri: selectedAppData.authentication.token_uri, + scope: selectedAppData.authentication.scope, + client_id: selectedAppData.authentication.client_id, + client_secret: selectedAppData.authentication.client_secret, + grant_type: selectedAppData.authentication.grant_type, + } : { + type: "", + } + ) + } + } + + function Heading(props) { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: 40 } }, + props.children + ); + return ( + + {props.level !== 1 ? ( + + ) : null} + {element} + + ); + } + + const getAppDocs = (appname, location, version) => { + fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + //toast("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { + setSelectedMeta(responseJson.meta) + } + + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + // Translate into markdown ![]() + const imgRegex = / ({ + ...prevState, + documentation: newdata, + })); + } + } + } + + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleDecodeOfOpenApiData = (data) => { + var appexists = false; + var parsedapp = {}; + + if (data.app !== undefined && data.app !== null) { + var parsedBaseapp = ""; + try { + parsedBaseapp = base64_decode(data.app); + } catch (e) { + parsedBaseapp = data; + } + + parsedapp = JSON.parse(parsedBaseapp); + parsedapp.name = parsedapp.name.replaceAll("_", " "); + + appexists = + parsedapp.name !== undefined && + parsedapp.name !== null && + parsedapp.name.length !== 0; + if(parsedapp?.id.length > 0){ + setSelectedAppData(parsedapp) + handleAppAuthenticationType(parsedapp) + const apptype = selectedAppData?.generated === false ? "python" : "openapi" + getAppDocs(parsedapp.name, apptype, parsedapp.version); + setAuthenticationModalOpen(true); + } + } + + if (data.openapi === undefined || data.openapi === null) { + return; + } + + var parsedDecoded = ""; + try { + parsedDecoded = base64_decode(data.openapi); + } catch (e) { + parsedDecoded = data; + } + + parsedapp = JSON.parse(parsedDecoded); + data = + parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body); + }; + + const getAppData = (appid) => { + if (appid === undefined || appid === null || appid.length === 0) { + return; + } + const url = `${globalUrl}/api/v1/apps/${appid}/config`; + + fetch(url, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + toast.error("Failed to get app data or App doesn't. Please contact support@shuffler.io"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + handleDecodeOfOpenApiData(responseJson); + } else { + toast.error("Failed to get app data or App doesn't exist"); + } + }) + .catch((error) => { + console.error("error for app is :", error); + }); + }; + + const UpdateAppAuthentication = (data) => { + if (data === undefined || data === null) { + return; + } + const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid); + if (filteredData.length === 0) { + setAppAuthentication([]); + setSelectedAuthentication({}); + } else { + setAppAuthentication(filteredData); + setSelectedAuthentication(filteredData[0]); + } + }; + + const HandleAppAuthentication = ()=>{ + + const url = `${globalUrl}/api/v1/apps/authentication`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson.success === true) { + UpdateAppAuthentication(responseJson.data); + } else { + toast.error("Failed to get app authentication data"); + } + }).catch((error) => { + console.error("error for app is :", error); + }); + } + + const handleAppAuthenticationNew = (data) => () => { + + const appid = data.objectID; + if (appid > 0) { + setAppId(appid); + } + if (appid.length > 0) { + toast.info(`Getting authentication for ${data.name}. Please wait...`); + getAppData(appid); + HandleAppAuthentication(); + } + } + + const AuthenticationData = (props) => { + const selectedApp = props.app; + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + // workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = ""; + } + } + + const setNewAppAuth = (appAuthData, refresh) => { + setSelectedAuthentication(appAuthData); + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + headers["Org-Id"] = userdata?.active_org?.id + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: headers, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + + if (response.status === 400) { + toast.error("Failed setting new auth. Please try again", { + "autoClose": true, + }) + } + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast.error("Error: " + responseJson.reason, { + "autoClose": false, + }) + + } else { + HandleAppAuthentication() + setAuthenticationModalOpen(false) + getAppAuthentication() + } + }) + .catch((error) => { + console.log("New auth error: ", error.toString()); + }); + }; + + const handleSubmitCheck = () => { + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[paramkey].value !== undefined && + selectedApp.authentication.parameters[paramkey].value !== null && + selectedApp.authentication.parameters[paramkey].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = selectedApp.authentication.parameters[paramkey].value; + } else { + if ( + selectedApp.authentication.parameters[paramkey].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = "false"; + } else { + toast( + "Field " + + selectedApp.authentication.parameters[paramkey].name + + " can't be empty" + ); + return; + } + } + } + } + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (let authkey in newAuthOption.fields) { + const value = newAuthOption.fields[authkey]; + newFields.push({ + "key": authkey, + "value": value, + }); + } + + newAuthOption.fields = newFields + setNewAppAuth(newAuthOption) + } + + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
+ +
+ Authentication for {selectedApp.name.replaceAll("_", " ", -1)} +
+
+ + + What is app authentication? + +
+ These are required fields for authenticating with {selectedApp.name} +
+ Label for you to remember + { + authenticationOption.label = event.target.value; + }} + /> + +
+ {selectedApp.authentication.parameters.map((data, index) => { + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + } + + + return ( +
+ + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
+ ); + })} + + + + + +
+ ); + }; + + const authenticationModal = authenticationModalOpen ? ( + {setSelectedMeta(undefined)}} + PaperProps={{ + style: { + pointerEvents: "auto", + color: "white", + minWidth: 1100, + minHeight: 700, + maxHeight: 700, + padding: 15, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > +
+ { selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + {`Documentation + + ) : ( + + {`Documentation + + )} +
+ + + + + + { + setAuthenticationModalOpen(false); + }} + > + + +
+
+ {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? + + : + + } +
+
+ {selectedAppData.documentation === undefined || + selectedAppData.documentation === null || + selectedAppData.documentation.length === 0 ? ( + +
+ + {selectedAppData?.description} + +
+ + +
+ + There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! + + +
+ + + Want to help the making of, or improve this app?{" "} +
+ + Join the community on Discord! + +
+ + + Want to help change this app directly? + + {selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + + Check it out on Github! + + + + ) : ( + + + + Check it out on Github! + + + + )} +
+ ) : ( +
+ {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
+
+ {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
+ )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
+
+ {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
+ {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
+ )} +
+
+ : null} + + + {selectedAppData.documentation} + +
+ )} +
+
+
+) : null; + + return ( +
+ {authenticationModal} + {hits.length === 0 && searchQuery.length >= 0 ? ( +
+ + No Apps Found + +
+ ) : ( + +
+ {hits.map((data, index) => { + const appUrl = isCloud + ? `/apps/${data.objectID}?queryID=${data.__queryID}` + : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`; + + return ( + + + + setMouseHoverIndex(index)} + onMouseLeave={() => setMouseHoverIndex(-1)} + > + +
+
+ + + + + + ); + })} +
+ + )} +
+ ); + +}; + +const CustomSearchBox = connectSearchBox(SearchBox); +const CustomHits = connectHits(Hits); diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx index c65cf1cf..d6fc1f42 100644 --- a/frontend/src/components/AppFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -198,7 +198,7 @@ export const findSpecificApp = (framework, inputcategory) => { id: "", } } else { - console.log("findSpecificApp: unknown category: ", category) + //console.log("findSpecificApp: unknown category: ", category) } return null diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index b0577937..8a5d9a18 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -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 ( { }} > - {data.tags.slice(0, 1).map((tag, tagIndex) => ( + {data?.tags?.slice(0, 1)?.map((tag, tagIndex) => ( {normalizedString(tag)} {tagIndex < 1 ? ", " : ""} @@ -565,7 +569,7 @@ const AppGrid = (props) => { ) : (
{data.tags && - data.tags.map((tag, tagIndex) => ( + data?.tags?.map((tag, 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 && (
- {topCategories.map((data, index) => ( + {topCategories?.map((data, index) => (
- {topTags && topTags.length > 0 && topTags.map((data, index) => ( + {topTags && topTags.length > 0 && topTags?.map((data, index) => ( + + + + ) : 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 ( { } }} > + {deleteModal} { fontFamily: theme?.typography?.fontFamily }} > + {/* About {app?.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())} + */} @@ -457,7 +567,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => {
{app?.name} { flexDirection: "row", alignItems: "center", }}> - - {newAppname} - + + + {newAppname} + + { ) : null} + {(userdata?.id === app?.owner)? ( + + + + ): null} + + {(canEditApp && app?.generated) && ( + {canEditApp ? "Edit" : "Fork"} + + )}
@@ -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" ) - } + */}
)} @@ -703,7 +845,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => { alignItems: 'center', mb: 3 }}> - {usecaseLoading ? ( + {/*usecaseLoading ? ( @@ -752,7 +894,7 @@ const AppModal = ({ open, onClose, app, globalUrl }) => { {foundAppUsecase?.name || "Search for a Usecase"} - )} + )*/}
diff --git a/frontend/src/components/AppSearch1.jsx b/frontend/src/components/AppSearch1.jsx new file mode 100644 index 00000000..3947f640 --- /dev/null +++ b/frontend/src/components/AppSearch1.jsx @@ -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 ( +
+
+ +
+ { + 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 }} + /> +
+ + ), + }} + 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' : ''*/} + +
+ ) + } + + const Hits = ({ hits, currentRefinement }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + var counted = 0 + + return ( + + {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 ( + { + 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") + } + }}> +
+ {data.name} + + {parsedname} + +
+
+ ) + })} +
+ ) + } + + const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(InputHits) + + return ( +
+ +
+ +
+
+ {open ? : null} +
+
+
+ ) +} + +export default Appsearch; diff --git a/frontend/src/components/AppSearchButtons.jsx b/frontend/src/components/AppSearchButtons.jsx index 4a4eef4e..cf9741cb 100644 --- a/frontend/src/components/AppSearchButtons.jsx +++ b/frontend/src/components/AppSearchButtons.jsx @@ -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 ( - diff --git a/frontend/src/components/AppStats.jsx b/frontend/src/components/AppStats.jsx new file mode 100644 index 00000000..d23d0833 --- /dev/null +++ b/frontend/src/components/AppStats.jsx @@ -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={ + + } + area={ + } + gradient={ + , + + ]} + /> + } + /> + } + gridlines={} />} + 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 ( +
+ + {inputname} + + } /> + } + /> + {/* + ( + + )} + /> + } + /> + } + /> + */} +
+ ) +} + + +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 = ( +
+
+ + + {widgetData.orgs} + + + Orgs + + + + + {widgetData.searches} + + + Searches + + + {/* + + + {widgetData.clicks} + + + Clicks + + + + + {widgetData.conversions} + + + Conversions + + + + + {widgetData.forks} + + + Forks + + + */} +
+ + {clickData === undefined || clickData === null || clickData?.length === 0 ? + null + : + + } + +
+ {conversionData === undefined || conversionData === null || conversionData?.length === 0 ? + null + : + + } +
+ ) + + const dataWrapper = ( +
{data}
+ ); + + return dataWrapper; +} + +export default AppStats; diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index dedacfe6..732a8a18 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -50,6 +50,9 @@ const Appsearch = props => { return (
{ 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) => {
)} -
+ {isCloud ? ( +
@@ -2545,6 +2546,7 @@ const Billing = memo((props) => {
+ ): null}
{ - 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) => { > - { editCache ? "Edit Cache" : "Add Cache" } + { editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`}
Key { autoFixJson(value) }} > - +
@@ -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) => { ); + 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 ? ( + 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, + }, + }, + }} + > + +
+ Select sub-org to distribute Datastore key +
+
+ + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; + return (
{modalView} + {cacheDistributionModal}
-

Shuffle Datastore

+

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

Datastore is a permanent key-value database for storing data that can be used cross-workflow.
You can store anything from lists of IPs to complex configurations.  { setValue("") }} > - Add Cache + Add Key + + {fileCategories !== undefined && + fileCategories !== null && + fileCategories.length > 1 ? ( + + + { + setShowFileCategoryPopup(false) + }} + > + File Categories + + Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category. + + + + + + + + ) : null} + +
+ {renderTextBox ? + + + + : + + + + } + + {renderTextBox && { + 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 + />}
{isSelectedDataStore? null : { overflowX: "auto", }}> - {["Key", "Value", "Actions", "Updated"].map((header, index) => ( + {["Key", "Value", "Actions", "Updated", "Distribution"].map((header, index) => ( { backgroundColor: "#212121", }} > - {Array(4) + {Array(5) .fill() .map((_, colIndex) => ( { )) : listCache?.length === 0 ? ( - - No Keys Found - + + {Array(5).fill().map((_, index) => ( + + ))} + + ): 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 ( { 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={( { setEditCache(true) setDataValue({ @@ -598,14 +1023,14 @@ const CacheView = memo((props) => { { 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) => { { deleteCache(orgId, data.key); //deleteFile(orgId); }} > - delete + + + + @@ -642,6 +1089,49 @@ const CacheView = memo((props) => { }} primary={new Date(data.edited * 1000).toISOString()} /> + {selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ? + + + + } + style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }} + /> + : + + { + setShowDistributionPopup(true) + if(data?.suborg_distribution?.length > 0){ + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedCacheKey(data.key) + }} + /> + + } + style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }} + /> + } ); })} @@ -653,4 +1143,4 @@ const CacheView = memo((props) => { ); }); -export default memo(CacheView); +export default memo(CacheView); diff --git a/frontend/src/components/CloudSyncTab.jsx b/frontend/src/components/CloudSyncTab.jsx new file mode 100644 index 00000000..08926570 --- /dev/null +++ b/frontend/src/components/CloudSyncTab.jsx @@ -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 ? + + : + + + 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 ( + +
+ { + setExpanded(prev => !prev); + if(showEdit){ + setShowEdit(false) + } + }} + > + + {primaryIcon} + + + {isCloud && userdata.support === true ? + + { + e.preventDefault(); + e.stopPropagation(); + console.log('expanded', expanded) + if (expanded){ + setExpanded(false) + } + if (showEdit) { + setShowEdit(false) + return + } + + console.log("Edit") + + setShowEdit(true) + }} + /> + + : null} + {userdata.support === true ?( + + + + ):( + + { + if (!isCloud || userdata.support !== true) { + return + } + + e.preventDefault(); + e.stopPropagation(); + + enableFeature() + }} + > + {secondaryIcon} + + + )} + + + {expanded ? +
+ + Usage:  + {props.data.limit === 0 ? ( + "Unlimited" + ) : ( + + {props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit} + + )} + + {/* + Data sharing: {props.data.data_collection} + */} + Description: {secondary} +
+ : null} + + + {showEdit ? + { + console.log("Submit") + submitEdit(e) + }}> + + + { + setNewValue(event.target.value) + }} + /> + + + + : null} +
+
+ ); + }; + 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 ( +
+
+

+ Cloud syncronization +

+ + What does
cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. + +
+ + {isCloud ? ( +
+
+ + Currently syncronizing:{" "} + {selectedOrganization.cloud_sync_active === true + ? True + : False} + + {selectedOrganization.cloud_sync_active ? ( + + Syncronization interval:{" "} + {selectedOrganization.sync_config.interval === 0 + ? "60" + : selectedOrganization.sync_config.interval} + + ) : null} + + Your Api key + + {userSettings?.apikey === undefined || userSettings?.apikey === null || userSettings?.apikey?.length <=0 ? ( + + ): +
+ + { + setShowApiKey(!showApiKey) + }} + > + {showApiKey ? : } + + + ) + }} + 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 ? ( + + ) : null} +
} +
+
+ ) : ( +
+
+ { + setCloudSyncApikey(event.target.value); + }} + /> + +
+ {orgSyncResponse.length > 0 ? ( + + Message from Shuffle Cloud: {orgSyncResponse} + + ) : null} +
+ )} + +

+ Features +

+ + Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. + + + {selectedOrganization.sync_features === undefined || + selectedOrganization.sync_features === null + ? + {[...Array(18)].map((_, i) => ( + +
+ +
+
+ ))} +
+ : 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: , + }; + + return ( + + + + ); + })} +
+
+ ); +}; + +export default CloudSyncTab; diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index ea8af7bd..a6ac8623 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -799,12 +799,16 @@ const ConfigureWorkflow = (props) => { >
{ + if (filled) { + return + } + setOpened(!opened); // Scroll to it @@ -865,6 +869,7 @@ const ConfigureWorkflow = (props) => { isLoggedIn={true} getAppAuthentication={undefined} + workflow={workflow} setFinalized={setFinalized} />
@@ -1377,7 +1382,7 @@ const ConfigureWorkflow = (props) => { return (
-
+
{setConfigureWorkflowModalOpen !== undefined ? @@ -1387,7 +1392,7 @@ const ConfigureWorkflow = (props) => { : null } -
+
{/* { {requiredActions.length > 0 ? ( - 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. {setConfigureWorkflowModalOpen !== undefined ? @@ -1434,6 +1439,13 @@ const ConfigureWorkflow = (props) => { ) })} + + {/* + + Once done, you may continue to the workflow. + + */} + ) : null} diff --git a/frontend/src/components/DetectionExplorer.jsx b/frontend/src/components/DetectionExplorer.jsx index fed09d26..4b73e0f0 100644 --- a/frontend/src/components/DetectionExplorer.jsx +++ b/frontend/src/components/DetectionExplorer.jsx @@ -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 ( @@ -284,7 +285,8 @@ const DetectionExplorer = (props) => {
- {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 ?
{
- : + : */} - } + {/**/} {detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ? - + 0 ? green : red}} /> diff --git a/frontend/src/components/DetectionRuleCard.jsx b/frontend/src/components/DetectionRuleCard.jsx index 986b227f..1a2e9760 100644 --- a/frontend/src/components/DetectionRuleCard.jsx +++ b/frontend/src/components/DetectionRuleCard.jsx @@ -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
diff --git a/frontend/src/components/EditOrgTab.jsx b/frontend/src/components/EditOrgTab.jsx new file mode 100644 index 00000000..c1cd11fc --- /dev/null +++ b/frontend/src/components/EditOrgTab.jsx @@ -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 ( +
+
+
+
+
+

Organization overview

+ + On this page organization admins can configure organisations, and sub-orgs (MSSP).{" "} + + Learn more + + +
+
+ + { + 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); + } + }); + }} + > + + + + {userdata.support === true ? + + {/**/} + + + + : null} +
+
+ + {/* {isCloud ? + + { + 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} + + + : null} */} +
+ + +
+
+ ) +} + +export default EditOrgTab; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index a7694562..b67bde93 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -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 ( { Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more - {/* -
- -
- */} +
+ +
{showUpload === true ?
@@ -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) => { - - { - setDueDate(newValue) - }} - /> -
- - Type - { - console.log("Data: ", e.target.value) - - innerWorkflow.workflow_type = e.target.value - setInnerWorkflow(innerWorkflow) - }} - > - } label="Trigger" /> - } label="Subflow" /> - } label="Standalone" /> - - - - - - { - 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 - /> - { - 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 - /> + { @@ -633,21 +572,21 @@ const EditWorkflow = (props) => { fullWidth /> - + - MSSP controls + Multi-Tenancy, Backups & Security - - - MSSP Suborg Distribution (beta - contact support@shuffler.io for more info) + + 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!) + {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 === "" ? - Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please make one 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 make one or get access to suborgs by another admin, then try again. : @@ -687,6 +626,7 @@ const EditWorkflow = (props) => { All {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 ( - + {image}{" "} @@ -738,22 +684,19 @@ const EditWorkflow = (props) => { })} : - + Create a sub-org to distribute workflows to suborgs. } - {/**/} - - - + Git Backup Repository - Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your default backup repository. Credentials are encrypted. Creates notifications if it fails. + Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' and be formatted, with removed images, to make diffing easy. Overrides your default backup repository. Credentials are encrypted. Creates notifications if it fails. @@ -895,6 +838,60 @@ const EditWorkflow = (props) => { +
+ + Result cleanup ({selectedCleanupActions.length === 0 ? "No cleanup yet" : selectedCleanupActions.length === 1 ? "Cleaning up 1 node" : `Cleaning up ${selectedCleanupActions.length} nodes`}) + + + + Beta Feature: 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. + + + + + +
+ @@ -1094,11 +1091,11 @@ const EditWorkflow = (props) => {
- 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`}) - 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. @@ -1146,41 +1143,130 @@ const EditWorkflow = (props) => {
-
- : null} - {!isEditing ? <> -
- } - 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) + + + + + Publishing + + + + + 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 creator 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. + + + + + { + setDueDate(newValue) }} /> + -
- : null} + + Type + { + console.log("Data: ", e.target.value) - - - + + + +
+ : null}
diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx new file mode 100644 index 00000000..cd0a9416 --- /dev/null +++ b/frontend/src/components/EnvironmentTab.jsx @@ -0,0 +1,1677 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import theme from "../theme.jsx"; +import { + Tooltip, + Typography, + Switch, + TextField, + Button, + ButtonGroup, + List, + ListItem, + ListItemText, + IconButton, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Checkbox, + Divider, + Tab, + Tabs, + Collapse, + Skeleton, + Grid, + Chip, + MenuItem, +} from "@mui/material"; +import { CopyToClipboard } from "../views/Docs.jsx" +import { + FileCopy as FileCopyIcon, + CheckCircle as CheckCircleIcon, + Cached as CachedIcon, + Cloud as CloudIcon, + Cancel as CancelIcon, + Help as HelpIcon, + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, +} from "@mui/icons-material"; +import { toast } from 'react-toastify'; +import { Context } from '../context/ContextApi.jsx'; +import { green, red } from '../views/AngularWorkflow.jsx' +import AppSearch from "../components/AppSearch1.jsx"; + +const EnvironmentTab = memo((props) => { + const { globalUrl, isCloud, userdata, selectedOrganization } = props; + const [environments, setEnvironments] = React.useState([]); + const [showArchived, setShowArchived] = React.useState(false); + const [modalUser, setModalUser] = React.useState({}); + const [loginInfo, setLoginInfo] = React.useState(""); + const [modalOpen, setModalOpen] = React.useState(false); + const [showLoader, setShowLoader] = useState(true) + const [commandController, setCommandController] = React.useState({ + pipelines: false, + proxies: false, + }) + const [installationTab, setInstallationTab] = React.useState(0); + const [isExpanded, setIsExpanded] = React.useState(false); + const [listItemExpanded, setListItemExpanded] = React.useState(-1); + const [, setUpdate] = React.useState(0); + const [showDistributionPopup, setShowDistributionPopup] = React.useState(false); + const [selectedEnvironment, setSelectedEnvironment] = React.useState(null); + const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); + const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined) + + + useEffect(() => { + getEnvironments(); + setModalUser({}); + }, []); + + const changeModalData = (field, value) => { + modalUser[field] = value; + }; + + // Horrible frontend fix for environments + const setDefaultEnvironment = (environment) => { + // FIXME - add more checks to this + toast("Setting default location to " + environment.Name); + var newEnv = []; + for (var key in environments) { + if (environments[key].id == environment.id) { + if (environments[key].archived) { + toast("Can't set archived to default"); + return; + } + + environments[key].default = true; + } else if ( + environments[key].default == true && + environments[key].id !== environment.id + ) { + environments[key].default = false; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + setTimeout(() => { + getEnvironments(); + }, 1500); + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getEnvironments(); + }, 1500); + } + }), + ) + .catch((error) => { + console.log("Error in backend data: ", error); + }); + }; + + const rerunCloudWorkflows = (environment) => { + toast("Starting execution reruns. This can run in the background."); + fetch(`${globalUrl}/api/v1/environments/${environment.id}/rerun`, { + method: "GET", + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } else { + toast(response.reason); + //toast("Aborted all dangling workflows"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //toast(error.toString()) + }); + } + + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + setEnvironments(responseJson); + setShowLoader(false) + // Helper info for users in case they have a large queue and don't know about queue flushing + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + if (responseJson.length === 1 && responseJson[0].Type !== "cloud") { + setListItemExpanded(0) + } + for (var i = 0; i < responseJson.length; i++) { + const env = responseJson[i]; + + // Check if queuesize is too large + if (env.queue !== undefined && env.queue !== null && env.queue > 100) { + toast("Queue size for " + env.name + " is very large. We recommend you to reduce it by flushing the queue before continuing."); + break + } + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const flushQueue = (name) => { + // Just use this one? + const url = globalUrl + "/api/v1/flush_queue"; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + + const deleteEnvironment = (environment) => { + // FIXME - add some check here ROFL + //const name = environment.name + + //toast("Modifying environment " + name) + //var newEnv = [] + //for (var key in environments) { + // if (environments[key].Name == name) { + // if (environments[key].default) { + // toast("Can't modify the default environment") + // return + // } + + // if (environments[key].type === "cloud" && !environments[key].archived) { + // toast("Can't modify cloud environments") + // return + // } + + // environments[key].archived = !environments[key].archived + // } + + // newEnv.push(environments[key]) + //} + const id = environment.id; + + //toast("Modifying environment " + environment.Name) + var newEnv = []; + for (var key in environments) { + if (environments[key].id == id) { + if (environments[key].default) { + toast("Can't modify the default environment. Change the default environment first."); + return; + } + + if (environments[key].type === "cloud" && !environments[key].archived) { + toast("Can't modify cloud environments"); + return; + } + + environments[key].archived = !environments[key].archived; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + const abortEnvironmentWorkflows = (environment) => { + //console.log("Aborting all workflows started >10 minutes ago, not finished"); + toast( + "Clearing the queue - this may take some time. A new will show up when finished.", + ); + + fetch( + `${globalUrl}/api/v1/environments/${environment.id}/stop?deleteall=true`, + { + method: "GET", + credentials: "include", + }, + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + toast("Failed aborting dangling workflows"); + return; + } else { + toast("Successfully cleared the queue"); + + getEnvironments(); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //toast(error.toString()) + }); + }; + + const changeRecommendation = (recommendation, action) => { + const data = { + action: action, + name: recommendation.name, + }; + + fetch(`${globalUrl}/api/v1/recommendations/modify`, { + 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) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + getEnvironments(); + } else { + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + toast("Failed change recommendation: ", responseJson.reason); + } else { + toast("Failed change recommendation"); + } + } + }) + .catch((error) => { + toast( + "Failed dismissing alert. Please contact support@shuffler.io if this persists.", + ); + }); + }; + + const getOrborusCommand = (environment) => { + if (environment.Type === "cloud") { + //toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.",) + return + } + + if ( + props.userdata.active_org === undefined || + props.userdata.active_org === null + ) { + return; + } + + const elementName = "copy_element_shuffle"; + var auth = + environment.auth === "" + ? "cb5st3d3Z!3X3zaJ*Pc" + : environment.auth + + // Escape exclamation marks for copying + auth = auth.replace("\\!", "!").replace(/!/g, "\\!") + + const newUrl = + globalUrl === "https://shuffler.io" + ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" + : globalUrl; + + var skipPipeline = false + if (commandController.pipelines === true) { + skipPipeline = true + } + + var addProxy = false + if (commandController.proxies === true) { + addProxy = true + } + + if (installationTab === 1) { + return (`docker run -d \\ + --restart=always \\ + --name="shuffle-orborus" \\ + --pull=always \\ + --volume "/var/run/docker.sock:/var/run/docker.sock" \\ + -e AUTH="${auth}" \\ + -e ENVIRONMENT_NAME="${environment.Name}" \\ + -e ORG="${environment.org_id}" \\ + -e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\ + -e SHUFFLE_SWARM_CONFIG=run \\ + -e SHUFFLE_LOGS_DISABLED=true \\ + -e BASE_URL="${newUrl}" \\${addProxy ? ` + -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` + -e SHUFFLE_SKIP_PIPELINES=true \\` : ""} + ghcr.io/shuffle/shuffle-orborus:latest + `) + } else if (installationTab === 2) { + return `https://shuffler.io/docs/configuration#kubernetes` + } + + const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\ + --restart=always \\ + --name="shuffle-orborus" \\ + --pull=always \\ + --volume "/var/run/docker.sock:/var/run/docker.sock" \\ + -e AUTH="${auth}" \\ + -e ENVIRONMENT_NAME="${environment.Name}" \\ + -e ORG="${props.userdata.active_org.id}" \\ + -e BASE_URL="${newUrl}" \\${addProxy ? ` + -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` + -e SHUFFLE_SKIP_PIPELINES=true \\` : ""} + ghcr.io/shuffle/shuffle-orborus:latest` + + return commandData + }; + + const submitEnvironment = (data) => { + // FIXME - add some check here ROFL + environments.push({ + name: data.environment, + type: "onprem", + }); + + // Just use this one? + var baseurl = globalUrl; + const url = baseurl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(environments), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error in input: " + responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + 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, + }, + }, + }} + > + + Add Location + + +
+ Location Name + + changeModalData("environment", event.target.value) + } + /> +
+ {loginInfo} {/* Assuming loginInfo is part of the relevant content */} +
+ + + + +
+ ); + + + const textColor = "#9E9E9E !important"; + + 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 queueSizeText = (queue) => { + if (queue === undefined || queue === null) return 0; + if (queue < 0) return 0; + if (queue > 1000) return ">1000"; + return queue; + }; + + const LocationActionModal = (props) => { + const { showLocationActionModal } = props + + const [searchQuery, setSearchQuery] = React.useState(""); + + if (showLocationActionModal === undefined || showLocationActionModal === null) { + return null + } + + if (showLocationActionModal?.open !== true) { + return null + } + + + + return ( + { + setShowLocationActionModal(undefined) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: 600, + minHeight: 500, + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + +
+ Select a job to send +
+
+ + + + Find app to re-download + + + + +
+ ) + } + + const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => { + const data = { + action: "suborg_distribute", + selected_suborgs: selectedSubOrg, + } + + const url = `${globalUrl}/api/v1/environments/${id}/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 environments"); + } else { + toast("Successfully updated environments!"); + setTimeout(() => { + getEnvironments(); + setShowDistributionPopup(false); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const changeDistribution = (id, selectedSubOrg) => { + + editEnvironmentConfig(id, [...new Set(selectedSubOrg)]) + } + + const EnvironmentDistributionModal = showDistributionPopup ? ( + 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, + }, + }, + }} + > + +
+ Select sub-org to distribute Environments +
+
+ + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; + + return ( +
+ {modalView} + {EnvironmentDistributionModal} +
+
+
+

Runtime Locations

+ + Decides which Orborus runtime location to run your workflows in. Previously called Environments.
If you have scale problems, check the docs or talk to our team: support@shuffler.io.  + + Learn more + +
+
+ + + setShowArchived(!showArchived)} + />{" "} + Show disabled + {/* */} +
+ + + {["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => { + + return ( + + ) + })} + + {showLoader + ? [...Array(6)].map((_, rowIndex) => ( + + {Array(9).fill(null).map((_, colIndex) => ( + + + + ))} + + )) + : environments?.length === 0 ? ( + + No Locations Found + + ):( + environments?.map((environment, index) => { + if (!showArchived && environment.archived) { + return null; + } + + if (environment.archived === undefined) { + return null; + } + + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + // Check if there's a notification for it in userdata.priorities + var showCPUAlert = false; + var foundIndex = -1; + if ( + userdata !== undefined && + userdata !== null && + userdata.priorities !== undefined && + userdata.priorities !== null && + userdata.priorities.length > 0 + ) { + foundIndex = userdata.priorities.findIndex( + (prio) => prio.name.includes("CPU") && prio.active === true, + ); + + if ( + foundIndex >= 0 && + userdata.priorities[foundIndex].name.endsWith( + environment.Name, + ) + ) { + showCPUAlert = true; + } + } + + const queueSize = + environment.queue !== undefined && environment.queue !== null + ? environment.queue < 0 + ? 0 + : environment.queue > 1000 + ? ">1000" + : environment.queue + : 0; + + + const orborusCommandWrapper = () => { + // Check the current text + const orborusCommand = document.getElementById("orborus_command") + if (orborusCommand === undefined || orborusCommand === null) { + return getOrborusCommand(environment) + } + + return orborusCommand.textContent + } + + const isDistributed = environment?.suborg_distribution?.length > 0 ? true : false; + + return ( + <> + + { + if (environment.Type === "cloud") { + toast("Cloud environments are not configurable. To see what is possible, create a new environment.") + return + } + + setListItemExpanded(listItemExpanded === index ? -1 : index) + }} + > + + + + ) : environment.run_type === "docker" ? ( + + + + ) : environment.run_type === "k8s" ? ( + + + + ) : ( + + + + ) + } + style={{ + minWidth: 80, + padding: "8px 8px 8px 0", + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + textAlign: "center", + display: "table-cell", + }} + /> + + + {environment.Type !== "cloud" + ? environment.running_ip === undefined || + environment.running_ip === null || + environment.running_ip.length === 0 + ? + "Not running. Click to get the start command that can be ran on your server." + : + IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus. + : + "Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions." + } + +
+
+ + Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"} + + } placement="top"> + + {environment.Type !== "cloud" && + (environment.running_ip === undefined || + environment.running_ip === null || + environment.running_ip.length === 0) + ? + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + : + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + } + + + } + /> + + + + + ) : ( + + + + + + ) + } + style={{ + minWidth: 60, + marginLeft: 20, + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + padding: 8, + display: "table-cell", + }} + /> + + + + + : + environment?.data_lake?.enabled && environment?.archived !== true ? ( + + + + + + + ) : ( + { + e.preventDefault() + e.stopPropagation() + + window.open("/detections/Sigma", "_blank") + }} + > + + + + + ) + } + style={{ + minWidth: 60, + marginLeft: 40, + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + display: "table-cell", + }} + /> + + + {environment.Name} + + )} + primaryTypographyProps={{ + style:{ + maxWidth: 150, + whiteSpace: 'nowrap', + overflow: "hidden", + textOverflow: 'ellipsis', + wordWrap: "break-word", + transition: "all 0.3s ease", + }}} + style={{ + minWidth: 120, + maxWidth: 150, + display: "table-cell", + }} + /> + + + + +
+ + + + + + + {environment.Type === "cloud" ? null : + + } + + + + {setIsExpanded(prev => !prev)}}> + {listItemExpanded === index ? : } + +
+
+ {selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ? + + + + } + style={{ textAlign: 'center', verticalAlign: 'middle', }} + /> + : + + { + e.stopPropagation() + setShowDistributionPopup(true) + if(environment?.suborg_distribution?.length > 0){ + setSelectedSubOrg(environment.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedEnvironment(environment.id) + }}> + + + + } +
+ + + +
+
+ + Self-Hosted Orborus instance + + + Orborus is the Shuffle queue handler that runs your hybrid workflows and manages pipelines. It can be run in Docker/k8s container on your server or in your cluster. Follow the steps below, and configure as need be. + + + { + setInstallationTab(inputValue) + }} + aria-label="disabled tabs example" + variant="scrollable" + scrollButtons="auto" + style={{textAlign: "center", marginTop: 25, }} + > + + Verbose (default) + + /> + + Scale + + /> + + k8s + + /> + + + {installationTab === 2 ? + + Check our Kubernetes documentation for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected. + + : + + 1. Ensure Docker is installed and the target server can reach '{globalUrl}' + + } + + + + {installationTab === 2 ? null : + "2. Run this command on the server you want to run workflows or store Pipeline data on"} + + + {installationTab === 2 ? null : +
+
+ + {getOrborusCommand(environment)} + + +
+ { + navigator.clipboard.writeText(orborusCommandWrapper()) + toast("Copied to clipboard") + }} + > + + +
+
+ + + Configure HTTP Proxies: { + if (commandController.proxies === undefined) { + commandController.proxies = true + } else { + commandController.proxies = !commandController.proxies + } + + setCommandController(commandController) + setUpdate(Math.random()) + }} + /> +
+ Disable Pipelines & Data Lake: { + if (commandController.pipelines === undefined) { + commandController.pipelines = true + } else { + commandController.pipelines = !commandController.pipelines + } + setCommandController(commandController) + setUpdate(Math.random()) + }} + /> +
+ } + + + {installationTab === 2 ? null : + + 3. Verify if the node is running. Try to refresh the page a little while after running the command. + + } + +
+
+ + + + + {showCPUAlert === false ? null : ( + +
+
+ + 90% CPU the server(s) hosting the Shuffle App + Runner (Orborus) was found. + + + Need help with High Availability and Scale?{" "} + + Read documentation + {" "} + and{" "} + + Get in touch + + . + +
+
+ +
+
+
+ )} + + ); + }) + ) } + +
+
+
+ + {showLocationActionModal !== undefined && showLocationActionModal !== null && showLocationActionModal?.open === true ? +
+ +
+ + : null } +
+ + + ) +}); + +export default EnvironmentTab; diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index 378bc532..f825fb7f 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -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) => { ) : null} +
{renderTextBox ? @@ -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 ( { placement="top" > { console.log("Workflow validation: ", workflow.validation) return (
+ {/* {workflow.errors !== undefined && workflow.errors !== null ?
General errors: {workflow.errors.length} @@ -656,11 +657,8 @@ const FixWorkflowValidationErrors = (props) => { })}
: null} - - - - {workflow.validation.errors !== undefined && workflow.validation.errors !== null ? + workflow.validation.errors !== undefined && workflow.validation.errors !== null ?
Validation errors: {workflow.validation.errors.length} {workflow.validation.errors.map((error, index) => { @@ -675,10 +673,12 @@ const FixWorkflowValidationErrors = (props) => { ) })}
- : null} + : null*/} + {/* Apps loaded: {apps.length} + */}
) diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index c6944955..432075ec 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -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(() => { > { 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(() => { }) - - { - handleClose(); - }} - style={{fontSize: 18}} - > - Use Cases - - @@ -537,7 +525,7 @@ useEffect(() => { - Version: 2.0.0-rc2 + Version: 2.0.0 @@ -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 = ( - { - 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, - }, - }} - > - - - - - - - - ); - 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 ? : null} { alignItems: "center", padding: "24px 16px 24px 27px", }} - > - - Shuffle Logo - + onMouseOver={()=>{ + if(window?.location?.pathname?.includes("/workflows/")) { + setExpandLeftNav(true) + } + }} + + onMouseLeave={()=>{ + if(window?.location?.pathname?.includes("/workflows/")) { + setExpandLeftNav(false) + } + }} + > + + + Shuffle Logo + + { - {!leftSideBarOpenByClick && setExpandLeftNav(true);}} onMouseLeave={()=>{!leftSideBarOpenByClick && setExpandLeftNav(false);setOpenAutocomplete(false);}}> + {(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(true)}} onMouseLeave={()=>{(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(false);setOpenAutocomplete(false)}}> { 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 }} > @@ -1206,12 +1214,12 @@ useEffect(() => { : "transparent"; }} > - { : "#C8C8C8" }} > - Discover + Content @@ -1263,7 +1271,7 @@ useEffect(() => { { to={"/forms"} style={{ ...hrefStyle, - pointerEvents: userdata?.support ? "auto" : "none", + pointerEvents: "auto", }} > + + + + + + + + + + + + + {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) => ( { }; export default LeftSideBar; + + +const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata}) => { + return ( + ( + { + 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, + }, + }} + > + + + + + + + + ) + ) +}); diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index b8d009a3..c29095e4 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -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) diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index e193460c..ed8ef8a1 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -465,7 +465,7 @@ const Header = (props) => { - + { handleClose(); @@ -1107,7 +1107,7 @@ const Header = (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}`; diff --git a/frontend/src/components/OrgHeaderNew.jsx b/frontend/src/components/OrgHeaderNew.jsx new file mode 100644 index 00000000..b319f3f3 --- /dev/null +++ b/frontend/src/components/OrgHeaderNew.jsx @@ -0,0 +1,520 @@ +import React, { useEffect, useState } from "react"; +import theme from "../theme.jsx"; +import { makeStyles } from "@mui/styles"; +import { toast } from 'react-toastify'; + +import { + Tooltip, + TextField, + FormControl, + InputLabel, + OutlinedInput, + Checkbox, + Select, + MenuItem, + Button, + ListItemText, + Dialog, + DialogActions, + DialogContent, + Divider, + DialogTitle, +} from "@mui/material"; + +import AvatarEditor from "react-avatar-editor"; + + +import { + AddAPhotoOutlined as AddAPhotoOutlinedIcon, + ZoomInOutlined as ZoomInOutlinedIcon, + ZoomOutOutlined as ZoomOutOutlinedIcon, + Loop as LoopIcon, + AddPhotoAlternate as AddPhotoAlternateIcon, +} from "@mui/icons-material"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, +} from "@mui/icons-material"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const defaultImage = + "data:image/png;base64,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"; + +const OrgHeader = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, + handleEditOrg, + isEditOrgTab, + serverside, + } = props; + + const classes = useStyles(); + + var upload = ""; + const defaultBranch = "master"; + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + + const [file, setFile] = React.useState(""); + const [fileBase64, setFileBase64] = React.useState( + selectedOrganization.image + ); + useEffect(() => { + if (selectedOrganization.image !== undefined && selectedOrganization.image !== null && selectedOrganization.image.length > 0) { + setFileBase64(selectedOrganization.image); + setFile(selectedOrganization.image); + } + }, [selectedOrganization]); + + const removeImage = () => { + setFile(""); + setFileBase64(""); + setCroppedData(defaultImage); + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + defaultImage, + { + 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, + }, + [], + ) + }; + + const surfaceColor = "#27292D"; + const inputColor = "#383B40"; + + const bodyDivStyle = { + margin: "auto", + width: "900px", + }; + + const appIconStyle = { + marginLeft: "5px", + }; + + const dividerStyle = { + marginBottom: "10px", + marginTop: "10px", + height: "1px", + width: "100%", + backgroundColor: "grey", + }; + + if (file !== "") { + const img = document.getElementById("logo"); + var canvas = document.createElement("canvas"); + canvas.width = 174; + canvas.height = 174; + var ctx = canvas.getContext("2d"); + + img.onload = function () { + // img, x, y, width, height + //ctx.drawImage(img, 174, 174) + //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight) + //ctx.drawImage(img, 0, 0, 174, 174) + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + 0, + 0, + canvas.width, + canvas.height + ); + + const canvasUrl = canvas.toDataURL(); + if (canvasUrl !== fileBase64) { + setFileBase64(canvasUrl); + selectedOrganization.image = canvasUrl; + setSelectedOrganization(selectedOrganization); + } + }; + } + + + var image = ""; + const editHeaderImage = (event) => { + const file = event.target.value; + const actualFile = event.target.files[0]; + const fileObject = URL.createObjectURL(actualFile); + setFile(fileObject); + }; + + //console.log("USER: ", userdata) + const orgSaveButton = ( + + + + ); + + const [imageUploadError, setImageUploadError] = React.useState(""); + const [openImageModal, setOpenImageModal] = React.useState(false); + const [scale, setScale] = React.useState(1); + const [rotate, setRotation] = React.useState(0); + const [disableImageUpload, setDisableImageUpload] = React.useState(true); + const [croppedData, setCroppedData] = React.useState(defaultImage); + const [imageData, setImageData] = useState(selectedOrganization?.image?.lenth > 0 ? selectedOrganization?.image : defaultImage) + + React.useEffect(() => { + if (file.length > 0) { + setCroppedData(file); + } else if (fileBase64 !== undefined && fileBase64 !== null && fileBase64.length > 0) { + setCroppedData(fileBase64); + } else { + setCroppedData(defaultImage); + } + + if((imageData !== selectedOrganization?.image) && selectedOrganization?.image?.length > 0){ + setImageData(selectedOrganization?.image) + } + }, [selectedOrganization, file]); + + + const alternateImg = ( + { + upload.click(); + }} + /> + ); + + const zoomIn = () => { + setScale(scale + 0.1); + }; + + const zoomOut = () => { + setScale(scale - 0.1); + }; + + const rotation = () => { + setRotation(rotate + 10); + }; + + const onPositionChange = () => { + setDisableImageUpload(false); + }; + + const onCancelSaveAppIcon = () => { + setOpenImageModal(false); + setImageUploadError(""); + }; + + let editor; + const setEditorRef = (imgEditor) => { + editor = imgEditor; + }; + + + const onSaveAppIcon = () => { + const canvas = editor.getImageScaledToCanvas(); + const newImageData = canvas.toDataURL(); + setCroppedData(newImageData); // Update croppedData with the new image data + setOpenImageModal(false); + setDisableImageUpload(true); + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + newImageData, + { + 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, + }, + [], + ) + }; + + const imageInfo = ( + + ); + + const errorText = imageUploadError.length > 0 ? ( +
Error: {imageUploadError}
+ ) : null; + + + const imageUploadModalView = openImageModal ? ( + + + +
Upload Organization Image
+
+ {errorText} + + setRotation(0)} + /> + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ ) : null; + + + return ( +
+
+ +
{ + setOpenImageModal(true); + }} + > + (upload = ref)} + onChange={(e) => { + const reader = new FileReader(); + reader.onload = (event) => { + setCroppedData(event.target.result); + }; + reader.readAsDataURL(e.target.files[0]); + }} + /> + {imageInfo} +
+ {imageUploadModalView} +
+
+
+ +
+
+ +
+
+
+
+ ); +}; + +export default OrgHeader; diff --git a/frontend/src/components/OrgHeaderexpandedNew.jsx b/frontend/src/components/OrgHeaderexpandedNew.jsx new file mode 100644 index 00000000..9a3cda99 --- /dev/null +++ b/frontend/src/components/OrgHeaderexpandedNew.jsx @@ -0,0 +1,1165 @@ +import React, { memo, useEffect, useState } from "react"; + +import { makeStyles } from "@mui/styles"; +import { toast } from "react-toastify" +import theme from '../theme.jsx'; +//import { useAlert + +import { + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Chip, + Link, + Typography, + Switch, + Select, + MenuItem, + Divider, + ListItemText, + TextField, + Button, + Tabs, + Tab, + Grid, + Autocomplete, +} from "@mui/material"; + +import { + Icon as IconButton, + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, + CookieSharp, +} from "@mui/icons-material"; +import CloudSyncTab from "./CloudSyncTab.jsx"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const OrgHeaderexpandedNew = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, + selectedStatus, + setSelectedStatus, + isEditOrgTab + } = props; + + const classes = useStyles(); + const defaultBranch = "main"; + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 300, + borderRadius: 20, + overflowY: "scroll", + }, + }, + getContentAnchorEl: () => null, + }; + + const [orgName, setOrgName] = useState(selectedOrganization?.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + const [appDownloadUrl, setAppDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo === undefined || + selectedOrganization.defaults.app_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo + ); + + const [openNotification, setOpenNotification] = React.useState(false); + + const handleStatusChange = (event) => { + const { value } = event.target; + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization?.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: 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 [appDownloadBranch, setAppDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.app_download_branch === undefined || + selectedOrganization.defaults.app_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.app_download_branch + ); + const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.workflow_download_repo === undefined || + selectedOrganization.defaults.workflow_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-workflows" + : selectedOrganization.defaults.workflow_download_repo + ); + const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch === undefined || + selectedOrganization.defaults.workflow_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch + ); + + const [documentationReference, setDocumentationReference] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.documentation_reference === undefined || + selectedOrganization.defaults.documentation_reference.length === 0 + ? "" + : selectedOrganization.defaults.documentation_reference + ); + const [newsletter, setNewsletter] = React.useState( + selectedOrganization.defaults === undefined + ? true + : selectedOrganization.defaults.newsletter === undefined || + selectedOrganization.defaults.newsletter.length === 0 + ? true + : !selectedOrganization.defaults.newsletter + ) + + const [weeklyRecommendations, setWeeklyRecommendations] = React.useState( + selectedOrganization.defaults === undefined + ? true + : selectedOrganization.defaults.weekly_recommendations === undefined || + selectedOrganization.defaults.weekly_recommendations.length === 0 + ? true + : !selectedOrganization.defaults.weekly_recommendations + ) + + const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo) + const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch) + const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username) + const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token) + const [regionStatus, setRegionStatus] = useState(); + + useEffect(() => { + + if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) { + setDocumentationReference(selectedOrganization?.defaults?.documentation_reference) + } + + if (uploadRepo !== selectedOrganization?.defaults?.workflow_upload_repo) { + setUploadRepo(selectedOrganization?.defaults?.workflow_upload_repo) + } + + if (uploadBranch !== selectedOrganization?.defaults?.workflow_upload_branch) { + setUploadBranch(selectedOrganization?.defaults?.workflow_upload_branch) + } + + if (uploadUsername !== selectedOrganization?.defaults?.workflow_upload_username) { + setUploadUsername(selectedOrganization?.defaults?.workflow_upload_username) + } + + if (uploadToken !== selectedOrganization?.defaults?.workflow_upload_token) { + setUploadToken(selectedOrganization?.defaults?.workflow_upload_token) + } + }, [selectedOrganization]) + + useEffect(() => { + if (selectedOrganization !== undefined && selectedOrganization !== null) { + if ((orgName === undefined || orgName === null || orgName.length === 0) && selectedOrganization?.name !== orgName) { + setOrgName(selectedOrganization?.name) + } + if ((orgDescription === undefined || orgDescription === null || orgDescription.length === 0) && selectedOrganization?.description !== orgDescription) { + setOrgDescription(selectedOrganization?.description) + } + } + }, [selectedOrganization]) + + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config, + lead_info, + ) => { + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + lead_info: lead_info, + mfa_required: selectedOrganization?.mfa_required, + Billing: selectedOrganization?.Billing, + }; + + 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 handleSendChangeRegionMail = (region) => { + if (selectedOrganization === undefined || selectedOrganization === null) { + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + return + } + + const regionToCloudRegion = { + 'US': 'us-west2', + 'EU': 'europe-west3', + 'CA': 'northamerica-northeast1', + 'UK': 'europe-west2', + 'EU-2': 'europe-west3' + }; + + const destinationRegion = regionToCloudRegion[region] || region; + + var data = { + dst_region: destinationRegion + } + + toast.info("Sending request for changing region to " + region) + + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change/region/request`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + }).then((response) => { + if (response.status !== 200) { + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + } else { + toast.success("Successfully sent request for region change. We will process the move and contact you shortly.") + } + }).catch((err) => { + console.log(err) + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + }) + } + + const setSelectedRegion = (region) => { + + // send a POST request to /api/v1/orgs/{org_id}/region with the region as the body + if (region === "US") { + region = "us-west2" + } else if (region === "EU") { + region = "europe-west2" + } else if (region === "CA") { + region = "northamerica-northeast1" + } else if (region === "UK") { + region = "europe-west2" + } else if (region === "EU-2") { + region = "europe-west3" + } + + var data = { + dst_region: region + } + + toast.info("Changing region to " + region + "...This may take a few minutes.") + + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/change/region`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + timeOut: 1000 + }).then((response) => { + if (response.status !== 200) { + toast("Failed to change region!") + } + else { + toast("Region changed successfully! Reloading in 5 seconds..") + // Reload the page in 2 seconds + setTimeout(() => { + window.location.reload() + }, 5000) + + } + + return response.json(); + }) + } + + + const orgSaveButton = ( + + + + ); + + return ( +
+ + {/* + + + + Email settings + + + Enable or disable email notifications for your organization. + + +
+ + { + setNewsletter(e.target.checked) + }} + disabled={!isCloud} + /> + + + { + setWeeklyRecommendations(e.target.checked) + }} + disabled={!isCloud} + /> +
+
+
+ */} + + + + +
+
+
+
+
+ Name + { + if ((orgName !== selectedOrganization?.name) && (orgName !== "")) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + } + }} + onChange={(e) => { + if (e.target.value.length > 100) { + toast("Choose a shorter name."); + return; + } + + setOrgName(e.target.value); + }} + color="primary" + InputProps={{ + style: { + color: "white", + height: "35px", + fontSize: "1em", + borderRadius: 4, + }, + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + }} + /> +
+ {userdata?.support ? ( +
+
Status
+ + + +
+ ) : null} + + + {isCloud ? ( +
+ Change Region + +
+ ) : null} +
+
+ About +
+ { + if ((orgDescription !== selectedOrganization?.description) && (orgDescription !== "")) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + } + }} + onChange={(e) => { + setOrgDescription(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + height: 89, + borderRadius: 4, + }, + }} + /> +
+
+ +
+
+ + Preferences + + + {/*isCloud ? + + : null*/} +
+ {/* + + Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows + + */} + + + + + Org Documentation reference + + + Add a URL that is added as a link, pointing to any external documentation page you want. + + + { + if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: documentationReference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + } + }} + onChange={(e) => { + setDocumentationReference(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35 + }, + }} + /> + + + + + Workflow Backup Repository + + Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the repo root in the /orgId/workflow-status/workflowId.json format. MSSP: If suborg exists, this will automatically be applied for them as well (not retroactive). Credentials are encrypted. + + + + + Repository for workflow backup + { + setUploadRepo(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + Branch + { + setUploadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + + + Username for backup of workflows + { + setUploadUsername(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + Git token/password + { + setUploadToken(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + type="password" + /> + + + + + {/*isCloud ? null : */} + + {isCloud ? null : ( + + + App Download URL + { + setAppDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + App Download Branch + { + setAppDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download URL + { + setWorkflowDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download Branch + { + setWorkflowDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {/* + + {expanded ? + + : + + } + + */} + +
+ {orgSaveButton} +
+
+ ) +} + +export default OrgHeaderexpandedNew; + +const RegionChangeModal = memo(({ selectedOrganization, setSelectedRegion, userdata, handleSendChangeRegionMail }) => { + // Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1" + // var regions = ["us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"] + const regionMapping = { + "US": "us", + "EU-2": "eu", + "CA": "ca", + "UK": "gb", + }; + + //let regiontag = "UK"; + let regiontag = "UK"; + let regionCode = "gb"; + + const regionsplit = selectedOrganization?.region_url?.split("."); + + if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) { + const namesplit = regionsplit[0]?.split("/"); + regiontag = namesplit[namesplit.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } + + return ( + + {/* Region */} + + + ); +}) diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx new file mode 100644 index 00000000..3fc2cbee --- /dev/null +++ b/frontend/src/components/OrganizationTab.jsx @@ -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 ; + case 'sso': + return + case `notifications`: + case `priorities`: + return ( + + ); + case 'billingstats' : + case 'billing' : + return ( + + ); + case 'branding(beta)': + return ; + // case 'analytics': + // return ; + default: + return ; + } + }; + + return ( +
+
+ {['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding (Beta)'].map((tabName, index) => ( + +
+ +
+
+ ))} +
+
+ {renderContent()} +
+
+ ); +}; + +export default OrganizationTab; diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index df84ad89..4732efc2 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -13,313 +13,364 @@ import { green, yellow, red } from "../views/AngularWorkflow.jsx" //import { useAlert import { - Chip, - ButtonGroup, - Popper, - TextField, - TextareaAutosize, - Drawer, - Button, - Paper, - Tabs, - InputAdornment, - Tab, - ButtonBase, - Tooltip, - Select, - MenuItem, - Divider, - Dialog, - Modal, - DialogActions, - DialogTitle, - InputLabel, - DialogContent, - FormControl, - IconButton, - Menu, - Input, - FormGroup, - FormControlLabel, - Typography, - Checkbox, - Breadcrumbs, - CircularProgress, - Switch, - Collapse, - Autocomplete, + Chip, + ButtonGroup, + Popper, + TextField, + TextareaAutosize, + Drawer, + Button, + Paper, + Tabs, + InputAdornment, + Tab, + ButtonBase, + Tooltip, + Select, + MenuItem, + Divider, + Dialog, + Modal, + DialogActions, + DialogTitle, + InputLabel, + DialogContent, + FormControl, + IconButton, + Menu, + Input, + FormGroup, + FormControlLabel, + Typography, + Checkbox, + Breadcrumbs, + CircularProgress, + Switch, + Collapse, + Autocomplete, Box } from "@mui/material"; import { - HelpOutline as HelpOutlineIcon, - OpenInFull as OpenInFullIcon, - Description as DescriptionIcon, - GetApp as GetAppIcon, - Search as SearchIcon, - ArrowUpward as ArrowUpwardIcon, - Visibility as VisibilityIcon, - Done as DoneIcon, - Close as CloseIcon, - Error as ErrorIcon, - FindReplace as FindreplaceIcon, - ArrowLeft as ArrowLeftIcon, - Cached as CachedIcon, - DirectionsRun as DirectionsRunIcon, - Add as AddIcon, - Polymer as PolymerIcon, - FormatListNumbered as FormatListNumberedIcon, - Create as CreateIcon, - PlayArrow as PlayArrowIcon, - AspectRatio as AspectRatioIcon, - MoreVert as MoreVertIcon, - Apps as AppsIcon, - Schedule as ScheduleIcon, - FavoriteBorder as FavoriteBorderIcon, - Pause as PauseIcon, - Delete as DeleteIcon, - AddCircleOutline as AddCircleOutlineIcon, - Save as SaveIcon, - KeyboardArrowLeft as KeyboardArrowLeftIcon, - KeyboardArrowRight as KeyboardArrowRightIcon, - ArrowBack as ArrowBackIcon, - Settings as SettingsIcon, - LockOpen as LockOpenIcon, - ExpandMore as ExpandMoreIcon, - VpnKey as VpnKeyIcon, + HelpOutline as HelpOutlineIcon, + OpenInFull as OpenInFullIcon, + Description as DescriptionIcon, + GetApp as GetAppIcon, + Search as SearchIcon, + ArrowUpward as ArrowUpwardIcon, + Visibility as VisibilityIcon, + Done as DoneIcon, + Close as CloseIcon, + Error as ErrorIcon, + FindReplace as FindreplaceIcon, + ArrowLeft as ArrowLeftIcon, + Cached as CachedIcon, + DirectionsRun as DirectionsRunIcon, + Add as AddIcon, + Polymer as PolymerIcon, + FormatListNumbered as FormatListNumberedIcon, + Create as CreateIcon, + PlayArrow as PlayArrowIcon, + AspectRatio as AspectRatioIcon, + MoreVert as MoreVertIcon, + Apps as AppsIcon, + Schedule as ScheduleIcon, + FavoriteBorder as FavoriteBorderIcon, + Pause as PauseIcon, + Delete as DeleteIcon, + AddCircleOutline as AddCircleOutlineIcon, + Save as SaveIcon, + KeyboardArrowLeft as KeyboardArrowLeftIcon, + KeyboardArrowRight as KeyboardArrowRightIcon, + ArrowBack as ArrowBackIcon, + Settings as SettingsIcon, + LockOpen as LockOpenIcon, + ExpandMore as ExpandMoreIcon, + VpnKey as VpnKeyIcon, AutoFixHigh as AutoFixHighIcon, - Circle as CircleIcon, + Circle as CircleIcon, SquareFoot as SquareFootIcon, Storage as StorageIcon, Check as CheckIcon, + PriorityHigh as PriorityHighIcon, + Restore as RestoreIcon, } from '@mui/icons-material'; export const useStyles = makeStyles({ - root: { - "& .MuiAutocomplete-listbox": { - border: "2px solid grey", - color: "white", - fontSize: 18, - "& li:nth-child(even)": { - backgroundColor: "#CCC", - }, - "& li:nth-child(odd)": { - backgroundColor: "#FFF", - }, - }, - }, - inputRoot: { - color: "white", - "&:hover .MuiOutlinedInput-notchedOutline": { - borderColor: "#f86a3e", - }, - }, + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid grey", + color: "white", + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: "white", + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, }); - + const openApiFieldDesc = "Generated by OpenAPI body example"; const ParsedAction = (props) => { - const { - workflow, - files, - setWorkflow, - setAction, - setSelectedAction, - setUpdate, - appActionArguments, - selectedApp, - workflowExecutions, - setSelectedResult, - selectedAction, - setSelectedApp, - setSelectedTrigger, - setSelectedEdge, - setCurrentView, - cy, - setAuthenticationModalOpen, - setVariablesModalOpen, - setCodeModalOpen, - selectedNameChange, - rightsidebarStyle, - showEnvironment, - selectedActionEnvironment, - environments, - setNewSelectedAction, - appApiViewStyle, - globalUrl, - setSelectedActionEnvironment, - requiresAuthentication, - hideExtraTypes, - scrollConfig, - setScrollConfig, - authenticationType, - appAuthentication, - getAppAuthentication, - actionDelayChange, - getParents, - isCloud, - lastSaved, - setLastSaved, - setShowVideo, - toolsAppId, - aiSubmit, + const { + workflow, + files, + setWorkflow, + setAction, + setSelectedAction, + setUpdate, + appActionArguments, + selectedApp, + workflowExecutions, + setSelectedResult, + selectedAction, + setSelectedApp, + setSelectedTrigger, + setSelectedEdge, + setCurrentView, + cy, + setAuthenticationModalOpen, + setVariablesModalOpen, + setCodeModalOpen, + selectedNameChange, + rightsidebarStyle, + showEnvironment, + selectedActionEnvironment, + environments, + setNewSelectedAction, + appApiViewStyle, + globalUrl, + setSelectedActionEnvironment, + requiresAuthentication, + hideExtraTypes, + scrollConfig, + setScrollConfig, + authenticationType, + appAuthentication, + getAppAuthentication, + actionDelayChange, + getParents, + isCloud, + lastSaved, + setLastSaved, + setShowVideo, + toolsAppId, + aiSubmit, - expansionModalOpen, - setExpansionModalOpen, + expansionModalOpen, + setExpansionModalOpen, + fixExample, - listCache, - setActiveDialog, - authGroups, - apps, - setEditorData, - setcodedata, - setAiQueryModalOpen, - } = props; + listCache, + setActiveDialog, + authGroups, + apps, + setEditorData, + setcodedata, + setAiQueryModalOpen, - let navigate = useNavigate(); - const classes = useStyles(); + suborgWorkflows, + originalWorkflow, + } = props; - const [hideBody, setHideBody] = React.useState(false) - const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false) - const [appActionName, setAppActionName] = React.useState(selectedAction?.label); - const [delay, setDelay] = React.useState(selectedAction?.execution_delay || 0); - const [prevActionName, setPrevActionName] = React.useState(selectedAction?.label); - const [fieldCount, setFieldCount] = React.useState(0); - const [hiddenDescription, setHiddenDescription] = React.useState(true); - const [hiddenParameters, setHiddenParameters] = React.useState(true); - const [autoCompleting, setAutocompleting] = React.useState(false); - const [selectedActionParameters, setSelectedActionParameters] = React.useState(selectedAction?.parameters || []); - const [selectedVariableParameter, setSelectedVariableParameter] = React.useState(""); - const [paramUpdate, setParamUpdate] = React.useState(""); - const [actionlist, setActionlist] = React.useState([]); - const [jsonList, setJsonList] = React.useState([]); - const [showDropdown, setShowDropdown] = React.useState(false); - const [showDropdownNumber, setShowDropdownNumber] = React.useState(0); - const [showAutocomplete, setShowAutocomplete] = React.useState(false); - const [menuPosition, setMenuPosition] = useState(null); - const [uiBox, setUiBox] = useState(null); - const isIntegration = selectedAction.app_id === "integration" + let navigate = useNavigate() + const classes = useStyles() - useEffect(() => { - if (setLastSaved !== undefined) { - setLastSaved(false) - } - }, [expansionModalOpen]) + const [hideBody, setHideBody] = React.useState(false) + const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false) + const [appActionName, setAppActionName] = React.useState(selectedAction?.label); + const [delay, setDelay] = React.useState(selectedAction?.execution_delay || 0); + const [prevActionName, setPrevActionName] = React.useState(selectedAction?.label); + const [fieldCount, setFieldCount] = React.useState(0); + const [hiddenDescription, setHiddenDescription] = React.useState(true); + const [hiddenParameters, setHiddenParameters] = React.useState(true); + const [autoCompleting, setAutocompleting] = React.useState(false); + const [selectedActionParameters, setSelectedActionParameters] = React.useState(selectedAction?.parameters || []); + const [selectedVariableParameter, setSelectedVariableParameter] = React.useState(""); + const [paramUpdate, setParamUpdate] = React.useState(""); + const [actionlist, setActionlist] = React.useState([]); + const [jsonList, setJsonList] = React.useState([]); + const [showDropdown, setShowDropdown] = React.useState(false); + const [showDropdownNumber, setShowDropdownNumber] = React.useState(0); + const [showAutocomplete, setShowAutocomplete] = React.useState(false); + const [menuPosition, setMenuPosition] = useState(null); + const [uiBox, setUiBox] = useState(null); + const [parentAction, setParentAction] = useState(null); + const isIntegration = selectedAction.app_id === "integration" + const isAgent = selectedAction.app_id === "shuffle_agent" + const [distributeAuthToSuborgs, setDistributeAuthToSuborgs] = useState(selectedAction?.selectedAuthentication?.suborg_distributed || false) + useEffect(() => { + if (setLastSaved !== undefined) { + setLastSaved(false) + } + }, [expansionModalOpen]) - useEffect(() => { - // Changes the order of params to show in order: - // auth, required, optional - var changed = false - if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) { - return - } + /* + useEffect(() => { + // This will have the OLD selectedAction, not the new one huh? + // How do we map the fields correctly? + if (selectedAction === undefined || selectedAction === null) { + console.log("Selected action is undefined") + return + } - // Fixing required fields with a shitty structure :) - if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated === true && selectedAction !== undefined && selectedAction !== null && selectedAction.name !== undefined && selectedAction.name !== null && selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length > 0 && (selectedAction.required_body_fields === undefined || selectedAction.required_body_fields === null || selectedAction.required_body_fields.length === 0)) { - // Check for required fields - for (var actionkey in selectedApp.actions) { - var action = selectedApp.actions[actionkey] - if (action.name === selectedAction.name) { - selectedAction.required_body_fields = action.required_body_fields - break + console.log("Selected action: ", selectedAction?.name, selectedAction) + + }, [selectedAction]) + */ + + useEffect(() => { + // Changes the order of params to show in order: + // auth, required, optional + var changed = false + if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) { + return + } + + if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated !== true) { + return + } + + // Fixing required fields with a shitty structure :) + if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated === true && selectedAction !== undefined && selectedAction !== null && selectedAction.name !== undefined && selectedAction.name !== null && selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length > 0 && (selectedAction.required_body_fields === undefined || selectedAction.required_body_fields === null || selectedAction.required_body_fields.length === 0)) { + // Check for required fields + for (var actionkey in selectedApp.actions) { + var action = selectedApp.actions[actionkey] + if (action.name === selectedAction.name) { + selectedAction.required_body_fields = action.required_body_fields + break + } } } - } - // Check if missing parameters? - var auth = [] - var required = [] - var optional = [] + // Check if missing parameters? + var auth = [] + var required = [] + var optional = [] - var bodyfield = [] - var special_optional = [] - var generated_optional = [] + var bodyfield = [] + var special_optional = [] + var generated_optional = [] - var keyorder = [] - for (let paramkey in selectedActionParameters) { - var param = selectedActionParameters[paramkey] - keyorder.push(param.name) + var keyorder = [] - if (param.configuration) { - auth.push(param) - continue - } + for (let paramkey in selectedActionParameters) { + var param = selectedActionParameters[paramkey] + keyorder.push(param.name) - if (param?.value?.toLowerCase().includes("secret. replace")) { - param.value = "" - } + if (param?.configuration === true) { + auth.push(param) + continue + } + if (param?.value === undefined || param?.value === null) { + console.log("Invalid value: ", param) + continue + } - if (selectedApp?.generated === true && param?.name === "body") { - param.required = true - bodyfield.push(param) - continue - } + if (typeof param.value === "array") { + param.value = param.value.join(",") + } - if (param.required === false && param.name.startsWith("${") && param.name.endsWith("}")) { - // Check if it's a required param - param.autocompleted = false - if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { - if (selectedAction.required_body_fields.includes(param.name)) { - param.required = true + if (typeof param.value === "object") { + try { + // Check if it's an array or object + if (param.value.length !== undefined) { + param.value = param.value.join(",") + } else { + param.value = JSON.stringify(param.value) + } + } catch (e) { + console.log("Error parsing JSON for param value: ", param, e) + continue } - } - } + } - if (param.required) { - required.push(param) - continue - } + if (param?.value?.toLowerCase()?.includes("secret. replace")) { + param.value = "" + } - if (param.name === "headers" || param.name === "queries") { - special_optional.push(param) - continue - } + if (selectedApp?.generated === true && param?.name === "body") { + param.required = true + bodyfield.push(param) + continue + } - if (hideBody && param?.description.includes("Generated")) { - continue - } + if (param?.required === false && param?.name?.startsWith("${") && param?.name?.endsWith("}")) { + // Check if it's a required param + param.autocompleted = true + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { - if (param.field_active === true) { - generated_optional.push(param) - continue - } + if (selectedAction.required_body_fields.includes(param.name)) { + param.required = true + } + } + } - optional.push(param) - } + if (param.required) { + required.push(param) + continue + } - // Sort order: auth > body(used for simple/advanced) > required > optional - // Optional field order: - // 1. headers & queries - // 2. other fields - // 3. generated fields & all else + if (param?.name === "headers" || param?.name === "queries") { + special_optional.push(param) + continue + } + + if (hideBody && param?.description?.includes("Generated")) { + continue + } + + if (param?.field_active === true) { + param.autocompleted = true + generated_optional.push(param) + continue + } + + optional.push(param) + } + + // Sort order: auth > body(used for simple/advanced) > required > optional + // Optional field order: + // 1. headers & queries + // 2. other fields + // 3. generated fields & all else - const newparams = auth - .concat(bodyfield) - .concat(required) - .concat(special_optional) - .concat(generated_optional) - .concat(optional) + const newparams = auth + .concat(bodyfield) + .concat(required) + .concat(generated_optional) + .concat(special_optional) + .concat(optional) - var newkeyorder = [] - for (let paramkey in newparams) { - //console.log("Param: ", newparams[paramkey]) + var newkeyorder = [] + for (let paramkey in newparams) { + //console.log("Param: ", newparams[paramkey]) - newkeyorder.push(newparams[paramkey].name) - } + newkeyorder.push(newparams[paramkey].name) + } - if (keyorder.join(",") !== newkeyorder.join(",")) { - //toast("KEYORDER CHANGED!") + if (keyorder.join(",") !== newkeyorder.join(",")) { + //console.log("KEYORDER CHANGED! DID ACTION AS WELL?", keyorder, newkeyorder) - setSelectedActionParameters(newparams) - selectedAction.parameters = newparams - setSelectedAction(selectedAction) - } - }, [selectedActionParameters]) + setSelectedActionParameters(newparams) + selectedAction.parameters = newparams + setSelectedAction(selectedAction) + } + }, [selectedActionParameters]) - useEffect(() => { - const shouldHide = localStorage.getItem("hideBody") + useEffect(() => { + const shouldHide = localStorage.getItem("hideBody") if (shouldHide !== null) { const ishiding = shouldHide !== "true" if (ishiding !== hideBody) { @@ -327,185 +378,238 @@ const ParsedAction = (props) => { } } - if (selectedActionEnvironment === undefined || selectedActionEnvironment === null || Object.keys(selectedActionEnvironment).length === 0) { + if (selectedActionEnvironment === undefined || selectedActionEnvironment === null || Object.keys(selectedActionEnvironment).length === 0) { - if (environments !== undefined && environments !== null && environments.length > 0) { - if (selectedAction.environment !== undefined && selectedAction.environment !== null) { + if (environments !== undefined && environments !== null && environments.length > 0) { + if (selectedAction.environment !== undefined && selectedAction.environment !== null) { - const foundenv = environments.find(env => env.id === selectedAction.environment || selectedAction.environment === env.Name) + const foundenv = environments.find(env => env.id === selectedAction.environment || selectedAction.environment === env.Name) - if (foundenv !== undefined && foundenv !== null) { - setSelectedActionEnvironment(foundenv) - } - } - } - } - }, []) + if (foundenv !== undefined && foundenv !== null) { + setSelectedActionEnvironment(foundenv) + } + } + } + } - const keywords = [ - "len(", - "lower(", - "upper(", - "trim(", - "split(", - "length(", - "number(", - "parse(", - "join(", - ]; + // Fix apps with fewer actions + if (selectedApp !== undefined && selectedApp !== null && selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length <= 1 && apps !== undefined && apps !== null && apps.length > 0) { + // 1. Check local storage (?) + // 2. Check the "apps" list + const foundApp = apps.find(app => app.id === selectedApp.id) + if (foundApp !== undefined && foundApp !== null && foundApp.actions !== undefined && foundApp.actions !== null && foundApp.actions.length > 1) { + setSelectedApp(foundApp) + } + + } + }, []) - const getApp = (appId, setApp) => { - fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { - headers: { - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - //toast("Successfully GOT app "+appId) - } else { - toast("Failed getting app"); - } + const keywords = [ + "len(", + "lower(", + "upper(", + "trim(", + "split(", + "length(", + "number(", + "parse(", + "join(", + ]; - return response.json(); - }) - .then((responseJson) => { - console.log("RESPONSE: ", responseJson); + const getApp = (appId, setApp) => { + fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + toast("Failed getting app"); + } - const parsedapp = - responseJson.app !== undefined && responseJson.app !== null - ? JSON.parse(atob(responseJson.app)) - : {}; - console.log("PARSED: ", parsedapp); - //data = parsedapp.body === undefined ? parsedapp : parsedapp.body + return response.json(); + }) + .then((responseJson) => { + console.log("RESPONSE: ", responseJson); - if ( - setApp && - parsedapp.actions !== undefined && - parsedapp.actions !== null - ) { - console.log("Inside first if"); - if ( - selectedApp.versions !== undefined && - selectedApp.versions !== null - ) { - parsedapp.versions = selectedApp.versions; - } + const parsedapp = + responseJson.app !== undefined && responseJson.app !== null + ? JSON.parse(atob(responseJson.app)) + : {}; + console.log("PARSED: ", parsedapp); + //data = parsedapp.body === undefined ? parsedapp : parsedapp.body - if ( - selectedApp.loop_versions !== undefined && - selectedApp.loop_versions !== null - ) { - parsedapp.loop_versions = selectedApp.loop_versions; - } + if ( + setApp && + parsedapp.actions !== undefined && + parsedapp.actions !== null + ) { + console.log("Inside first if"); + if ( + selectedApp.versions !== undefined && + selectedApp.versions !== null + ) { + parsedapp.versions = selectedApp.versions; + } - // Find authentication, and if it works? - // If authentication has less OR more fields, it has to change - //console.log(selected + if ( + selectedApp.loop_versions !== undefined && + selectedApp.loop_versions !== null + ) { + parsedapp.loop_versions = selectedApp.loop_versions; + } - console.log("Inside first if2"); - var foundAction = parsedapp.actions.find( - (action) => - action.name.toLowerCase() === selectedAction.name.toLowerCase() - ); - if (foundAction !== null && foundAction !== undefined) { - var foundparams = []; - for (let [paramkey,paramkeyval] in Object.entries(foundAction.parameters)) { - const param = foundAction.parameters[paramkey]; + // Find authentication, and if it works? + // If authentication has less OR more fields, it has to change + //console.log(selected - const foundParam = selectedAction.parameters.find( - (item) => item.name.toLowerCase() === param.name.toLowerCase() - ); - if (foundParam === undefined) { - console.log("COULDNT find Param: ", param); - } else { - foundAction.parameters[paramkey] = foundParam; - } - //foundparams.push(param.name) - } - } else { - toast("Couldn't find action " + selectedAction.name); - } + console.log("Inside first if2"); + var foundAction = parsedapp.actions.find( + (action) => + action.name.toLowerCase() === selectedAction.name.toLowerCase() + ); + if (foundAction !== null && foundAction !== undefined) { + var foundparams = []; + for (let [paramkey, paramkeyval] in Object.entries(foundAction.parameters)) { + const param = foundAction.parameters[paramkey]; - selectedAction.errors = []; - selectedAction.is_valid = true; + const foundParam = selectedAction.parameters.find( + (item) => item.name.toLowerCase() === param.name.toLowerCase() + ); + if (foundParam === undefined) { + console.log("COULDNT find Param: ", param); + } else { + foundAction.parameters[paramkey] = foundParam; + } + //foundparams.push(param.name) + } + } else { + toast("Couldn't find action " + selectedAction.name); + } - // Updating params for the new action - selectedAction.parameters = foundAction.parameters; - selectedAction.app_id = appId; - selectedAction.app_version = parsedapp.app_version; + selectedAction.errors = []; + selectedAction.is_valid = true; - setSelectedAction(selectedAction); - setSelectedApp(parsedapp); - } - }) - .catch((error) => { - toast(error.toString()); - }); - }; + // Updating params for the new action + selectedAction.parameters = foundAction.parameters; + selectedAction.app_id = appId; + selectedAction.app_version = parsedapp.app_version; + setSelectedAction(selectedAction); + setSelectedApp(parsedapp); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + const changeDistribution = (data) => { + editAuthenticationConfig(data.id, "suborg_distribute") + } - const defineStartnode = () => { - if (cy === undefined) { - return; - } + const editAuthenticationConfig = (id, parentAction) => { + const data = { + id: id, + action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", + } - var oldstartnode = cy.getElementById(workflow.start); - if (oldstartnode.length > 0) { - oldstartnode[0].data("isStartNode", false); - var oldnodecnt = workflow.actions.findIndex( - (a) => a.id === workflow.start - ); - if (workflow.actions[oldnodecnt] !== undefined) { - workflow.actions[oldnodecnt].isStartNode = false; - } - } + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; - var newstartnode = cy.getElementById(selectedAction.id); - if (newstartnode.length > 0) { - newstartnode[0].data("isStartNode", true); - var newnodecnt = workflow.actions.findIndex( - (a) => a.id === selectedAction.id - ); - console.log("NEW NODE CNT: ", newnodecnt); - if (workflow.actions[newnodecnt] !== undefined) { - workflow.actions[newnodecnt].isStartNode = true; - console.log(workflow.actions[newnodecnt]); - } - } + 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 appauth"); + } else { + if (distributeAuthToSuborgs) { + toast.success("Successfully updated auth"); + } else { + toast.success("Successfully distributed auth to suborgs"); + } + setTimeout(() => { + getAppAuthentication(); + setDistributeAuthToSuborgs(!distributeAuthToSuborgs) + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; - // Find branches with triggers as source nodes - // Move these targets to be the new node - // Set arrows pointing to new startnode with errors - //for (var key in workflow.branches) { - // var item = workflow.branches[key] - // if (item.destination_id === oldstartnode[0].data()["id"]) { - // var curbranch = cy.getElementById(item.id) - // if (curbranch.length > 0) { - // //console.log(curbranch[0].data()) - // //curbranch[0].data("target", selectedAction.id) - // //curbranch[0].data("hasErrors", true) - // //workflow.branches[key].destination_id = selectedAction.id - // //console.log(curbranch[0].data()) - // } - // } - //} + const defineStartnode = () => { + if (cy === undefined) { + return; + } - setUpdate("start_node" + selectedAction.id); - workflow.start = selectedAction.id; - setWorkflow(workflow); - //setStartNode(selectedAction.id) - }; + var oldstartnode = cy.getElementById(workflow.start); + if (oldstartnode.length > 0) { + oldstartnode[0].data("isStartNode", false); + var oldnodecnt = workflow.actions.findIndex( + (a) => a.id === workflow.start + ); + if (workflow.actions[oldnodecnt] !== undefined) { + workflow.actions[oldnodecnt].isStartNode = false; + } + } + var newstartnode = cy.getElementById(selectedAction.id); + if (newstartnode.length > 0) { + newstartnode[0].data("isStartNode", true); + var newnodecnt = workflow.actions.findIndex( + (a) => a.id === selectedAction.id + ); + console.log("NEW NODE CNT: ", newnodecnt); + if (workflow.actions[newnodecnt] !== undefined) { + workflow.actions[newnodecnt].isStartNode = true; + console.log(workflow.actions[newnodecnt]); + } + } - useEffect(() => { + // Find branches with triggers as source nodes + // Move these targets to be the new node + // Set arrows pointing to new startnode with errors + //for (var key in workflow.branches) { + // var item = workflow.branches[key] + // if (item.destination_id === oldstartnode[0].data()["id"]) { + // var curbranch = cy.getElementById(item.id) + // if (curbranch.length > 0) { + // //console.log(curbranch[0].data()) + // //curbranch[0].data("target", selectedAction.id) + // //curbranch[0].data("hasErrors", true) + // //workflow.branches[key].destination_id = selectedAction.id + // //console.log(curbranch[0].data()) + // } + // } + //} + + setUpdate("start_node" + selectedAction.id); + workflow.start = selectedAction.id; + setWorkflow(workflow); + //setStartNode(selectedAction.id) + }; + + useEffect(() => { // Only set app action name if it has changed if (selectedAction.label !== appActionName) { setAppActionName(selectedAction.label); - const shouldHide = localStorage.getItem("hideBody") + const shouldHide = localStorage.getItem("hideBody") if (shouldHide !== null) { const ishiding = shouldHide !== "true" if (ishiding !== hideBody) { @@ -514,71 +618,91 @@ const ParsedAction = (props) => { } } - if(selectedAction.label !== prevActionName){ + if (selectedAction.label !== prevActionName) { setPrevActionName(selectedAction.label) } - + // Only set delay if it has changed const newDelay = selectedAction?.execution_delay || 0; if (newDelay !== delay) { setDelay(newDelay); } - + // Only set selected action parameters if they have changed if (selectedAction?.parameters?.length > 0 && selectedAction.label !== appActionName) { //console.log("PARAMS CHANGED DURING APPCHANGE: ", selectedAction.parameters) setSelectedActionParameters(selectedAction.parameters); } - + // Only set selected variable parameter if it is null or undefined if (!selectedVariableParameter && workflow.workflow_variables?.length > 0) { setSelectedVariableParameter(workflow.workflow_variables[0].name); } - },[selectedAction,selectedApp,setNewSelectedAction,workflow, workflowExecutions, getParents]) + + if (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0 && originalWorkflow?.id !== undefined && originalWorkflow?.id !== null && originalWorkflow?.id !== workflow?.id && originalWorkflow?.actions !== undefined && originalWorkflow?.actions !== null && originalWorkflow?.actions.length > 0) { + // Due to ID remapping of actions not happening, this is easy + for (var key in originalWorkflow.actions) { + const curparentAction = originalWorkflow.actions[key] + + if (curparentAction.id === selectedAction.id) { + if (curparentAction.parameters === undefined || curparentAction.parameters === null || curparentAction.parameters.length === 0) { + console.log("Parent parameters missing!") + break + } + + if (curparentAction.id !== parentAction?.id) { + setParentAction(curparentAction) + } + + break + } + } + } + }, [selectedAction, selectedApp, setNewSelectedAction, workflow, workflowExecutions, getParents]) useEffect(() => { - const newActionList = []; + const newActionList = []; const parentActionList = []; - // Process workflowExecutions - if (workflowExecutions.length > 0) { - for (let execution of workflowExecutions) { - const execArg = execution.execution_argument; - if (execArg && execArg.length > 0) { - const valid = validateJson(execArg); - if (valid.valid) { - newActionList.push({ - type: "Execution Argument", - name: "Execution Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: valid.result, - }) + // Process workflowExecutions + if (workflowExecutions.length > 0) { + for (let execution of workflowExecutions) { + const execArg = execution.execution_argument; + if (execArg && execArg.length > 0) { + const valid = validateJson(execArg); + if (valid.valid) { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: valid.result, + }) - break - } - } - } - } + break + } + } + } + } - // Add default Execution Argument if none were added - if (newActionList.length === 0) { - newActionList.push({ - type: "Execution Argument", - name: "Execution Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: "", - }) + // Add default Runtime Argument if none were added + if (newActionList.length === 0) { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "", + }) } // Look for cachekey if (newActionList.find((item) => item.type === "Shuffle DB") === undefined) { let cacheKey = { type: "Shuffle DB", - name: "Shuffle DB", + name: "Shuffle Datastore", value: "$shuffle_cache", highlight: "shuffle_cache", autocomplete: "shuffle_cache", @@ -601,105 +725,139 @@ const ParsedAction = (props) => { newActionList.push(cacheKey); } - // Process workflow variables - if (workflow.workflow_variables?.length > 0) { - for (let variable of workflow.workflow_variables) { - newActionList.push({ - type: "workflow_variable", - name: variable.name, - value: variable.value, - id: variable.id, - autocomplete: variable.name.split(" ").join("_"), - example: variable.value, - }); - } - } + // Process workflow variables + if (workflow.workflow_variables?.length > 0) { + for (let variable of workflow.workflow_variables) { + newActionList.push({ + type: "workflow_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: variable.value, + }); + } + } - // Process execution variables - if (workflow.execution_variables?.length > 0) { - for (let variable of workflow.execution_variables) { - let exampleOutput = ""; - for (let exec of workflowExecutions) { - const foundExec = exec.execution_variables?.find(exvar => exvar.name === variable.name); - if (foundExec?.value) { - exampleOutput = foundExec.value; - break; - } - } - newActionList.push({ - type: "execution_variable", - name: variable.name, - value: variable.value, - id: variable.id, - autocomplete: variable.name.split(" ").join("_"), - example: exampleOutput, - }); - } - } + // Process execution variables + if (workflow.execution_variables?.length > 0) { + for (let variable of workflow.execution_variables) { + let exampleOutput = ""; + for (let exec of workflowExecutions) { + const foundExec = exec.execution_variables?.find(exvar => exvar.name === variable.name); + if (foundExec?.value) { + exampleOutput = foundExec.value; + break; + } + } + newActionList.push({ + type: "execution_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: exampleOutput, + }); + } + } - // Process parent actions if getParents is provided - if (getParents) { - const parents = getParents(selectedAction); - if (parents.length > 1) { - const labels = []; - for (let parentNode of parents) { - if (parentNode.label !== "Execution Argument" && !labels.includes(parentNode.label)) { - labels.push(parentNode.label); - let exampleData = parentNode.example ?? ""; - if (!exampleData && workflowExecutions.length > 0) { - for (let exec of workflowExecutions) { - const foundResult = exec.results?.find(result => result.action.id === parentNode.id); - if (foundResult) { - const valid = validateJson(foundResult.result); - if (valid.valid && valid.result.success !== false) { - exampleData = valid.result; - break; - } - } - } - } + // Process parent actions if getParents is provided + if (getParents) { + const parents = getParents(selectedAction); + if (parents.length > 1) { + const labels = []; + for (let parentNode of parents) { + if (parentNode.label === "Runtime Argument" || labels.includes(parentNode.label)) { + continue + } - if (parentNode.label === undefined) { - parentNode.label = "" + labels.push(parentNode.label); + let exampleData = parentNode.example ?? ""; + if (parentNode?.app_name === "http") { + exampleData = "" + } + + if (workflowExecutions.length > 0) { + for (let exec of workflowExecutions) { + const foundResult = exec.results?.find(result => result?.action?.id === parentNode?.id); + if (foundResult) { + const valid = validateJson(foundResult.result); + if (valid.valid && valid.result.success !== false) { + exampleData = valid.result + break + } + } } + } - newActionList.push({ - type: "action", - id: parentNode.id, - name: parentNode.label, - autocomplete: parentNode.label.split(" ").join("_"), - example: exampleData, - }); + if (exampleData === "" && apps !== undefined && apps !== null && apps?.length > 0) { + // Check apps if it exists, then if it + const foundApp = apps?.find(app => app?.id === parentNode?.app_id) + if (foundApp !== undefined && foundApp !== null) { + if (foundApp?.generated === true || foundApp?.name === "http") { + const validationData = validateJson(`{ + "status": 200, + "body": { + "example": "json", + "values": "json" + }, + "url": "https://example.com", + "headers": { + "Content-Type": "application/json", + "Example-Header": "two" + }, + "cookies": { + "example": "session", + "__session": "sessionid" + }, + "success": true + }`) - parentActionList.push({ - type: "action", - id: parentNode.id, - name: parentNode.label, - autocomplete: parentNode.label.split(" ").join("_"), - example: exampleData, - }); + if (validationData.valid) { + exampleData = validationData.result + } + } + } + } - - } - } - } - } + if (parentNode.label === undefined) { + parentNode.label = "" + } - let newParameters = selectedAction?.parameters?.map((param) => { + newActionList.push({ + type: "action", + id: parentNode.id, + name: parentNode.label, + autocomplete: parentNode.label.split(" ").join("_"), + example: exampleData, + }); + + parentActionList.push({ + type: "action", + id: parentNode.id, + name: parentNode.label, + autocomplete: parentNode.label.split(" ").join("_"), + example: exampleData, + }); + } + } + } + + let newParameters = selectedAction?.parameters?.map((param) => { let paramvalue = param.value === undefined || param.value === null ? "" : param.value; let errorVars = []; - if(paramvalue.includes("$")){ + if (paramvalue.includes("$")) { let actions = workflow.actions?.map((action) => { - return "$"+action.label?.toLowerCase(); + return "$" + action.label?.toLowerCase(); }) - if(newActionList?.length > 0){ + if (newActionList?.length > 0) { let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase()); let notPresentAction = actions?.filter((action) => !appParentActions?.includes(action)) notPresentAction?.forEach((action) => { action = action.replace(" ", "_"); - if(paramvalue.includes(action)){ + if (paramvalue.includes(action)) { errorVars.push(action); // paramvalue = paramvalue.replace(action, "") // paramvalue = paramvalue.replace(/^\s*[\r\n]/gm, ""); @@ -709,280 +867,281 @@ const ParsedAction = (props) => { } let message = ""; - if(errorVars.length > 0){ - if(errorVars.length === 1){ + if (errorVars.length > 0) { + if (errorVars.length === 1) { message = errorVars[0] + " is not accessible in this action."; - }else{ + } else { message = errorVars.join(", ") + " are not accessible in this action."; } } - if (param?.configuration) { + if (param?.configuration && param?.name !== "url") { let regex = /(^|[^\\])\$/; if (regex.test(paramvalue)) { - if(message.length > 0){ + if (message.length > 0) { message += "\nUse \"\\$\" instead of \"$\" if you want to escape $ (1)"; - }else{ + } else { message = "Use \"\\$\" instead of \"$\" if you want to escape $ (2)"; } } } - return {...param, value: paramvalue, error: message} - }); - setSelectedActionParameters(newParameters); - setActionlist(newActionList); - }, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents,setNewSelectedAction]); + return { ...param, value: paramvalue, error: message } + }) + + setSelectedActionParameters(newParameters) + setActionlist(newActionList) + }, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents, setNewSelectedAction]); useEffect(() => { selectedNameChange(appActionName) if (actionDelayChange !== undefined) { - actionDelayChange(delay) + actionDelayChange(delay) } - },[appActionName,delay]) - - const handleParamChange = (event, count,data) => { - const newParams = [...selectedActionParameters]; - newParams.map((param) => { - if (param.name === data.name) { - param.value = event.target.value; + }, [appActionName, delay]) + + const handleParamChange = (event, count, data) => { + const newParams = [...selectedActionParameters]; + newParams.map((param) => { + if (param.name === data.name) { + param.value = event.target.value; + } + }) + setSelectedActionParameters(newParams); + setParamUpdate(event.target.value); + changeActionParameter(event, count, data) + } + + const calculateHelpertext = (input_data) => { + var helperText = "" + var looperText = "" + //const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) + var found = input_data.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g) + + if (found !== null && found !== undefined) { + var new_occurences = [] + for (let [key, keyval] in Object.entries(found)) { + if (found[key][0] !== "\\") { + new_occurences.push(found[key]) } - }) - setSelectedActionParameters(newParams); - setParamUpdate(event.target.value); - changeActionParameter(event, count, data) + } + + found = new_occurences.valueOf() } - const calculateHelpertext = (input_data) => { - var helperText = "" - var looperText = "" - //const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) - var found = input_data.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g) + if (found !== null) { + try { + // When the found array is empty. + for (let i = 0; i < found.length; i++) { + const variableSplit = found[i].split(".#") + if ((variableSplit.length - 1) > 1) { + //console.log("Larger than 1: ", variableSplit) + if (looperText.length === 0) { + looperText += "PS: Double looping (.#.#) may cause problems." + } + } - if (found !== null && found !== undefined) { - var new_occurences = [] - for (let [key,keyval] in Object.entries(found)) { - if (found[key][0] !== "\\") { - new_occurences.push(found[key]) + var foundSlice = false + for (let j = 0; j < actionlist.length; j++) { + //console.log("ACTION: ", found[i], actionlist[j]) + //console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase()) + if (found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()) { + //console.log("Found: ", found[i]) + // Validate path? + + foundSlice = true + } + } + + if (!foundSlice) { + if (!helperText.includes("Invalid variables")) { + helperText += "Invalid variables: " + } + helperText += found[i] + ", " + } + } + } catch (e) { + console.log("Parsing error: ", e) + } + } + + if (looperText.length > 0) { + if (helperText.length > 0) { + helperText += ". " + } + + helperText += looperText + } + + return helperText + } + + const changeActionParameter = (event, count, data, viewForceUpdate) => { + //console.log("Action change: ", selectedAction, data) + if (data.name.startsWith("${") && data.name.endsWith("}")) { + // PARAM FIX - Gonna use the ID field, even though it's a hack + const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); + + if (paramcheck !== undefined) { + // Escapes all double quotes + //var toReplace = event.target.value.trim() + var toReplace = event.target.value + + + if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) { + toReplace = toReplace.replaceAll('\\"', '"').replaceAll('"', '\\"') + } + + if ( + paramcheck["value_replace"] === undefined || + paramcheck["value_replace"] === null + ) { + paramcheck["value_replace"] = [ + { + key: data.name, + value: toReplace, + }, + ]; + + } else { + const subparamindex = paramcheck["value_replace"].findIndex( + (param) => param.key === data.name + ); + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + key: data.name, + value: toReplace, + }); + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace; } } - found = new_occurences.valueOf() - } + if (selectedActionParameters[count].value_replace === undefined) { + selectedActionParameters[count].value_replace = paramcheck + } - if (found !== null) { - try { - // When the found array is empty. - for (let i = 0; i < found.length; i++) { - const variableSplit = found[i].split(".#") - if ((variableSplit.length-1) > 1) { - //console.log("Larger than 1: ", variableSplit) - if (looperText.length === 0) { - looperText += "PS: Double looping (.#.#) may cause problems." - } - } + if (selectedAction?.parameters[count] !== undefined && selectedAction?.parameters[count].value_replace === undefined) { + selectedAction.parameters[count].value_replace = paramcheck + } - var foundSlice = false - for (let j = 0; j < actionlist.length; j++) { - //console.log("ACTION: ", found[i], actionlist[j]) - //console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase()) - if(found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()){ - //console.log("Found: ", found[i]) - // Validate path? + if (paramcheck["value_replace"] === undefined) { + selectedActionParameters[count]["value_replace"] = paramcheck - foundSlice = true - } - } - - if (!foundSlice) { - if (!helperText.includes("Invalid variables")) { - helperText+= "Invalid variables: " - } - helperText+= found[i] + ", " - } + if (selectedAction?.parameters[count] !== undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck + } + } else { + selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"]; + if (selectedAction?.parameters[count] !== undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"]; } - } catch (e) { - console.log("Parsing error: ", e) } + setSelectedAction(selectedAction); + //setUpdate(Math.random()) + return; } - - if (looperText.length > 0) { - if (helperText.length > 0) { - helperText += ". " - } - - helperText += looperText - } - - return helperText } - const changeActionParameter = (event, count, data, viewForceUpdate) => { - //console.log("Action change: ", selectedAction, data) - if (data.name.startsWith("${") && data.name.endsWith("}")) { - // PARAM FIX - Gonna use the ID field, even though it's a hack - const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); - - if (paramcheck !== undefined) { - // Escapes all double quotes - var toReplace = event.target.value.trim() - - if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) { - toReplace = toReplace.replaceAll('\\"', '"').replaceAll('"', '\\"') - } - - console.log("REPLACE WITH: ", toReplace); - if ( - paramcheck["value_replace"] === undefined || - paramcheck["value_replace"] === null - ) { - paramcheck["value_replace"] = [ - { - key: data.name, - value: toReplace, - }, - ]; - - } else { - const subparamindex = paramcheck["value_replace"].findIndex( - (param) => param.key === data.name - ); - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - key: data.name, - value: toReplace, - }); - } else { - paramcheck["value_replace"][subparamindex]["value"] = toReplace; - } - } - - if (selectedActionParameters[count].value_replace === undefined) { - selectedActionParameters[count].value_replace = paramcheck - } - - if (selectedAction?.parameters[count] !== undefined && selectedAction?.parameters[count].value_replace === undefined) { - selectedAction.parameters[count].value_replace = paramcheck - } - - if (paramcheck["value_replace"] === undefined) { - selectedActionParameters[count]["value_replace"] = paramcheck - - if (selectedAction?.parameters[count] !== undefined) { - selectedAction.parameters[count]["value_replace"] = paramcheck + if (event.target.value[event.target.value.length - 1] === "$") { + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); } - } else { - selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"]; - if (selectedAction?.parameters[count] !== undefined) { - selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"]; + } else { + if (showDropdown) { + setShowDropdown(false); } - } - setSelectedAction(selectedAction); - //setUpdate(Math.random()) - return; - } - } + } + // bad detection mechanism probably + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + console.log("GET THE LAST ARGUMENT FOR NODE!"); + // THIS IS AN EXAMPLE OF SHOWING IT + /* + + const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} + setJsonList(GetParsedPaths(inputdata, "")) + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + console.log(jsonList) + */ - if (event.target.value[event.target.value.length - 1] === "$") { - if (!showDropdown) { - setShowAutocomplete(false); - setShowDropdown(true); - setShowDropdownNumber(count); - } - } else { - if (showDropdown) { - setShowDropdown(false); - } - } + // Search for the item backwards + // 1. Reverse search backwards from . -> $ + // 2. Search the actionlist for the item + // 3. Find the data for the specific item - // bad detection mechanism probably - if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { - console.log("GET THE LAST ARGUMENT FOR NODE!"); - // THIS IS AN EXAMPLE OF SHOWING IT - /* - - const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} - setJsonList(GetParsedPaths(inputdata, "")) - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) + var curstring = ""; + var record = false; + for (let [key, keyval] in Object.entries(selectedActionParameters[count].value)) { + const item = selectedActionParameters[count].value[key]; + if (record) { + curstring += item; } - console.log(jsonList) - */ - // Search for the item backwards - // 1. Reverse search backwards from . -> $ - // 2. Search the actionlist for the item - // 3. Find the data for the specific item + if (item === "$") { + record = true; + curstring = ""; + } + } - var curstring = ""; - var record = false; - for (let [key,keyval] in Object.entries(selectedActionParameters[count].value)) { - const item = selectedActionParameters[count].value[key]; - if (record) { - curstring += item; - } + //console.log("CURSTRING: ", curstring) + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase(); + var actionItem = actionlist.find( + (data) => + data.autocomplete.split(" ").join("_").toLowerCase() === curstring + ); + if (actionItem !== undefined) { + console.log("Found item: ", actionItem); - if (item === "$") { - record = true; - curstring = ""; - } - } + //actionItem.example = actionItem.example.trim() + //actionItem.example = actionItem.example.split(" None").join(" \"None\"") + //actionItem.example = actionItem.example.split("\'").join("\"") - //console.log("CURSTRING: ", curstring) - if (curstring.length > 0 && actionlist !== null) { - // Search back in the action list - curstring = curstring.split(" ").join("_").toLowerCase(); - var actionItem = actionlist.find( - (data) => - data.autocomplete.split(" ").join("_").toLowerCase() === curstring - ); - if (actionItem !== undefined) { - console.log("Found item: ", actionItem); + var jsonvalid = true; + try { + const tmp = String(JSON.parse(actionItem.example)); + if ( + !actionItem.example.includes("{") && + !actionItem.example.includes("[") + ) { + jsonvalid = false; + } + } catch (e) { + jsonvalid = false; + } - //actionItem.example = actionItem.example.trim() - //actionItem.example = actionItem.example.split(" None").join(" \"None\"") - //actionItem.example = actionItem.example.split("\'").join("\"") + if (jsonvalid) { + setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")); - var jsonvalid = true; - try { - const tmp = String(JSON.parse(actionItem.example)); - if ( - !actionItem.example.includes("{") && - !actionItem.example.includes("[") - ) { - jsonvalid = false; - } - } catch (e) { - jsonvalid = false; - } - - if (jsonvalid) { - setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")); - - if (!showDropdown) { - setShowAutocomplete(false); - setShowDropdown(true); - setShowDropdownNumber(count); - } - } - } - } - } else { - if (jsonList.length > 0) { - setJsonList([]); - } - } + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); + } + } + } + } + } else { + if (jsonList.length > 0) { + setJsonList([]); + } + } selectedActionParameters[count].autocompleted = false selectedAction.parameters[count].autocompleted = false selectedActionParameters[count].value = event.target.value; selectedAction.parameters[count].value = event.target.value; - var forceUpdate = false + var forceUpdate = false if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { console.log("APIKEY - this shouldn't show up!") } @@ -999,8 +1158,8 @@ const ParsedAction = (props) => { selectedActionParameters[count].value = splitparsed[0] selectedAction.parameters[count].value = splitparsed[0] - selectedActionParameters[1].value = splitparsed[1] - selectedAction.parameters[1].value = splitparsed[1] + selectedActionParameters[1].value = splitparsed[1] + selectedAction.parameters[1].value = splitparsed[1] } else { // Remove .# and after const splitparsed = parsedvalue.split(".#") @@ -1028,233 +1187,233 @@ const ParsedAction = (props) => { } //console.log("END OF THIS THING") - //setUpdate(event.target.value) - }; + //setUpdate(event.target.value) + }; - - const changeActionParameterCodeMirror = (event, count, data) => { - if (data.startsWith("${") && data.endsWith("}")) { - // PARAM FIX - Gonna use the ID field, even though it's a hack - const paramcheck = selectedAction.parameters.find(param => param.name === "body") - if (paramcheck !== undefined) { - // Escapes all double quotes - const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); - console.log("REPLACE WITH: ", toReplace) - if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { - paramcheck["value_replace"] = [{ + + const changeActionParameterCodeMirror = (event, count, data) => { + if (data.startsWith("${") && data.endsWith("}")) { + // PARAM FIX - Gonna use the ID field, even though it's a hack + const paramcheck = selectedAction.parameters.find(param => param.name === "body") + if (paramcheck !== undefined) { + // Escapes all double quotes + const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); + console.log("REPLACE WITH: ", toReplace) + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [{ + "key": data.name, + "value": toReplace, + }] + + console.log("IN IF: ", paramcheck) + + } else { + const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) + if (subparamindex === -1) { + paramcheck["value_replace"].push({ "key": data.name, "value": toReplace, - }] - - console.log("IN IF: ", paramcheck) - + }) } else { - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - "key": data.name, - "value": toReplace, - }) - } else { - paramcheck["value_replace"][subparamindex]["value"] = toReplace - } + paramcheck["value_replace"][subparamindex]["value"] = toReplace } - //console.log("PARAM: ", paramcheck) - //if (paramcheck.id === undefined) { - // console.log("Normal paramcheck") - //} else { - // selectedActionParameters[count]["value_replace"] = paramcheck - // selectedAction.parameters[count]["value_replace"] = paramcheck - //} - - if (paramcheck["value_replace"] === undefined) { - selectedActionParameters[count]["value_replace"] = paramcheck - selectedAction.parameters[count]["value_replace"] = paramcheck - } else { - selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] - selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] - } - console.log("RESULT: ", selectedAction) - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - return } + //console.log("PARAM: ", paramcheck) + //if (paramcheck.id === undefined) { + // console.log("Normal paramcheck") + //} else { + // selectedActionParameters[count]["value_replace"] = paramcheck + // selectedAction.parameters[count]["value_replace"] = paramcheck + //} + + if (paramcheck["value_replace"] === undefined) { + selectedActionParameters[count]["value_replace"] = paramcheck + selectedAction.parameters[count]["value_replace"] = paramcheck + } else { + selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] + } + console.log("RESULT: ", selectedAction) + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + return } + } - if (event.target.value[event.target.value.length-1] === "$") { - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - } else { - if (showDropdown) { - setShowDropdown(false) - } + if (event.target.value[event.target.value.length - 1] === "$") { + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) } - - - // bad detection mechanism probably - if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) { - console.log("GET THE LAST ARGUMENT FOR NODE!") - // THIS IS AN EXAMPLE OF SHOWING IT - /* - const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} - setJsonList(GetParsedPaths(inputdata, "")) - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - console.log(jsonList) - */ - - // Search for the item backwards - // 1. Reverse search backwards from . -> $ - // 2. Search the actionlist for the item - // 3. Find the data for the specific item - - var curstring = "" - var record = false - for (let [key,keyval] in Object.entries(selectedActionParameters[count].value)) { - const item = selectedActionParameters[count].value[key] - if (record) { - curstring += item - } - - if (item === "$") { - record = true - curstring = "" - } - } - - //console.log("CURSTRING: ", curstring) - if (curstring.length > 0 && actionlist !== null) { - // Search back in the action list - curstring = curstring.split(" ").join("_").toLowerCase() - var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) - if (actionItem !== undefined) { - console.log("Found item: ", actionItem) - - //actionItem.example = actionItem.example.trim() - //actionItem.example = actionItem.example.split(" None").join(" \"None\"") - //actionItem.example = actionItem.example.split("\'").join("\"") - - var jsonvalid = true - try { - const tmp = String(JSON.parse(actionItem.example)) - if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - - if (jsonvalid) { - setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")) - - if (!showDropdown) { - setShowAutocomplete(false) - setShowDropdown(true) - setShowDropdownNumber(count) - } - } - } - } - } else { - if (jsonList.length > 0) { - setJsonList([]) - } + } else { + if (showDropdown) { + setShowDropdown(false) } - - setTimeout(() => { - selectedActionParameters[count].autocompleted = false - selectedAction.parameters[count].autocompleted = false - selectedActionParameters[count].value = data - selectedAction.parameters[count].value = data - }, 100); - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - //setUpdate(event.target.value) } - const changeActionParameterVariable = (fieldvalue, count) => { - //console.log("CALLED THIS ONE WITH VALUE!", fieldvalue) - //if (selectedVariableParameter === fieldvalue) { - // return - //} + // bad detection mechanism probably + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + console.log("GET THE LAST ARGUMENT FOR NODE!") + // THIS IS AN EXAMPLE OF SHOWING IT + /* + const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} + setJsonList(GetParsedPaths(inputdata, "")) + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + console.log(jsonList) + */ - setSelectedVariableParameter(fieldvalue); + // Search for the item backwards + // 1. Reverse search backwards from . -> $ + // 2. Search the actionlist for the item + // 3. Find the data for the specific item - selectedActionParameters[count].action_field = fieldvalue; - selectedAction.parameters = selectedActionParameters; + var curstring = "" + var record = false + for (let [key, keyval] in Object.entries(selectedActionParameters[count].value)) { + const item = selectedActionParameters[count].value[key] + if (record) { + curstring += item + } - setSelectedApp(selectedApp); - setSelectedAction(selectedAction); - setUpdate(fieldvalue); - }; + if (item === "$") { + record = true + curstring = "" + } + } - // Sets ACTION_RESULT things - const changeActionParameterActionResult = (fieldvalue, count) => { - //cy.nodes().forEach(function( ele ) { - // if (ele.data()["label"] === fieldvalue) { - // selectedActionParameters[count].action_field = ele.id() - // return - // } - //}); + //console.log("CURSTRING: ", curstring) + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase() + var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) + if (actionItem !== undefined) { + console.log("Found item: ", actionItem) - selectedActionParameters[count].action_field = fieldvalue; - selectedAction.parameters = selectedActionParameters; + //actionItem.example = actionItem.example.trim() + //actionItem.example = actionItem.example.split(" None").join(" \"None\"") + //actionItem.example = actionItem.example.split("\'").join("\"") - // FIXME - check if startnode + var jsonvalid = true + try { + const tmp = String(JSON.parse(actionItem.example)) + if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } - // Set value - setSelectedApp(selectedApp); + if (jsonvalid) { + setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")) - setSelectedAction(selectedAction); - setUpdate(Math.random()); - }; + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + } + } + } + } else { + if (jsonList.length > 0) { + setJsonList([]) + } + } - const changeActionParameterVariant = (data, count) => { - selectedActionParameters[count].variant = data; - selectedActionParameters[count].value = ""; + setTimeout(() => { + selectedActionParameters[count].autocompleted = false + selectedAction.parameters[count].autocompleted = false + selectedActionParameters[count].value = data + selectedAction.parameters[count].value = data + }, 100); + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + //setUpdate(event.target.value) + } - if (data === "ACTION_RESULT" && getParents !== undefined) { - var parents = getParents(selectedAction); - if (parents.length > 0) { - selectedActionParameters[count].action_field = parents[0].label; - } else { - selectedActionParameters[count].action_field = ""; - } - } else if (data === "WORKFLOW_VARIABLE") { - if ( - workflow.workflow_variables !== null && - workflow.workflow_variables !== undefined && - workflow.workflow_variables.length > 0 - ) { - selectedActionParameters[count].action_field = - workflow.workflow_variables[0].name; - } - } - selectedAction.parameters = selectedActionParameters; + const changeActionParameterVariable = (fieldvalue, count) => { + //console.log("CALLED THIS ONE WITH VALUE!", fieldvalue) + //if (selectedVariableParameter === fieldvalue) { + // return + //} - // This is a stupid workaround to make it refresh rofl - setSelectedAction({}); + setSelectedVariableParameter(fieldvalue); - if (setSelectedTrigger !== undefined) { - setSelectedTrigger({}); - setSelectedApp({}); - setSelectedEdge({}); - } - // FIXME - check if startnode + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; - // Set value - setSelectedApp(selectedApp); - setSelectedAction(selectedAction); - setUpdate(Math.random()); - }; + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(fieldvalue); + }; + + // Sets ACTION_RESULT things + const changeActionParameterActionResult = (fieldvalue, count) => { + //cy.nodes().forEach(function( ele ) { + // if (ele.data()["label"] === fieldvalue) { + // selectedActionParameters[count].action_field = ele.id() + // return + // } + //}); + + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; + + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; + + const changeActionParameterVariant = (data, count) => { + selectedActionParameters[count].variant = data; + selectedActionParameters[count].value = ""; + + if (data === "ACTION_RESULT" && getParents !== undefined) { + var parents = getParents(selectedAction); + if (parents.length > 0) { + selectedActionParameters[count].action_field = parents[0].label; + } else { + selectedActionParameters[count].action_field = ""; + } + } else if (data === "WORKFLOW_VARIABLE") { + if ( + workflow.workflow_variables !== null && + workflow.workflow_variables !== undefined && + workflow.workflow_variables.length > 0 + ) { + selectedActionParameters[count].action_field = + workflow.workflow_variables[0].name; + } + } + + selectedAction.parameters = selectedActionParameters; + + // This is a stupid workaround to make it refresh rofl + setSelectedAction({}); + + if (setSelectedTrigger !== undefined) { + setSelectedTrigger({}); + setSelectedApp({}); + setSelectedEdge({}); + } + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; const returnHelperText = (name, value) => { @@ -1266,7 +1425,7 @@ const ParsedAction = (props) => { var helperText = "" if (name.includes("url")) { if (value.includes("localhost") || value.includes("127.0.0.1")) { - helperText = "Can't use localhost in Shuffle. Please change to server's IP." + helperText = "Can't use localhost in Shuffle. Please change to server's IP." } } @@ -1306,7 +1465,7 @@ const ParsedAction = (props) => { if (selectedAction.name === "set_cache_value") { var actionKey = "" var actionValue = "" - for (let [key,keyval] in Object.entries(selectedActionParameters)) { + for (let [key, keyval] in Object.entries(selectedActionParameters)) { const param = selectedActionParameters[key] if (param.name === "key") { actionKey = param.value @@ -1322,7 +1481,7 @@ const ParsedAction = (props) => { } if (!actionKey.includes(".#") && actionValue.includes(".#")) { - return When the key ({actionKey}) is static, but the value is a list ({actionValue}), it will overwrite the list. You may be looking for the {}} style={{cursor: "pointer", color: "#FF8544", }}>Check Cache Contains action instead. + return When the key ({actionKey}) is static, but the value is a list ({actionValue}), it will overwrite the list. You may be looking for the { }} style={{ cursor: "pointer", color: "#FF8544", }}>Check Cache Contains action instead. } } @@ -1340,91 +1499,99 @@ const ParsedAction = (props) => { selectedAction.errors = ["Suggestion: " + suggestionText] } - return - - Tip: {suggestionText} - + return + + Tip: {suggestionText} + } - // FIXME: Issue #40 - selectedActionParameters not reset + // FIXME: Issue #40 - selectedActionParameters not reset if (Object.getOwnPropertyNames(selectedAction)?.length > 0 && selectedActionParameters?.length > 0) { - var wrapperapp = { - "id": "", - "name": "noapp", - "large_image": "", - } - - var actionname = selectedAction.name.toLowerCase() - if (actionname === "email" || actionname === "communication") { - actionname = "comms" - } - - if (isIntegration) { - - // Check if actionname uppercase is in the parsedDatatypeImages() dictionary - if (parsedDatatypeImages()[actionname.toUpperCase()] !== undefined) { - var newimage = parsedDatatypeImages()[actionname.toUpperCase()] - //newimage = - wrapperapp.large_image = newimage - } else { - console.log("Couldn't find actionname: ", actionname) + var wrapperapp = { + "id": "", + "name": "noapp", + "large_image": "", } - } - var authWritten = false; - var noAppSelected = false - if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > 0) { - var paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") - if (paramIndex === -1 || selectedAction.parameters[paramIndex].value === "" || selectedAction.parameters[paramIndex].value === "noapp") { - // Check the actual value and if it's the same - noAppSelected = true - } - } + var actionname = selectedAction.name.toLowerCase() + if (actionname === "email" || actionname === "communication") { + actionname = "comms" + } + + if (isAgent || isIntegration) { + // Check if actionname uppercase is in the parsedDatatypeImages() dictionary + if (isAgent) { + wrapperapp.large_image = theme.palette.singulBlackWhite + newimage = wrapperapp.large_image + } else if (isIntegration) { + wrapperapp.large_image = theme.palette.singulGreen + newimage = theme.palette.singulGreen + } else { + const uppercaseimage = parsedDatatypeImages()[actionname.toUpperCase()] + if (uppercaseimage !== undefined) { + var newimage = uppercaseimage + //newimage = + wrapperapp.large_image = newimage + } else { + console.log("Couldn't find actionname: ", actionname) + } + } + } + + var authWritten = false; + var noAppSelected = false + if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > 0) { + var paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex === -1 || selectedAction.parameters[paramIndex].value === "" || selectedAction.parameters[paramIndex].value === "noapp") { + // Check the actual value and if it's the same + noAppSelected = true + } + } } const ActionSelectOption = (actionprops) => { const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops; - const [hover, setHover] = React.useState(false); + const [hover, setHover] = React.useState(false); return (
setHover(true)} onMouseLeave={() => setHover(false)} - onClick={(event) => { - // event.preventDefault() - //setSelectedAction(actionprops) - //setShowActionList(false) - //setUpdate(Math.random()) - // - if (option !== undefined && option !== null) { - setNewSelectedAction({ - target: { - value: option.name - } - }); - } + onClick={(event) => { + // event.preventDefault() + //setSelectedAction(actionprops) + //setShowActionList(false) + //setUpdate(Math.random()) + // + if (option !== undefined && option !== null) { + setNewSelectedAction({ + target: { + value: option.name + } + }); + } - document.activeElement.blur(); + document.activeElement.blur(); - const disabledUiBox = localStorage.getItem("disabled_ui_box") - if (disabledUiBox === "true") { - } else { - setHiddenDescription(false) - } - }} + const disabledUiBox = localStorage.getItem("disabled_ui_box") + if (disabledUiBox === "true") { + } else { + setHiddenDescription(false) + } + }} > -
+
{ > {useIcon} - {newActionname} + {newActionname}
- {extraDescription.length > 0 ? - - {extraDescription} + {extraDescription.length > 0 ? + + {extraDescription} - : null} + : null}
) } - const sortByCategoryLabel = (a, b) => { - const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 - const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 + const sortByCategoryLabel = (a, b) => { + const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 + const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 - // Sort by existence and length of "category_label" - if (aHasCategoryLabel && !bHasCategoryLabel) { - return -1 - } else if (!aHasCategoryLabel && bHasCategoryLabel) { - return 1 - } else { - return 0 - } - } + // Sort by existence and length of "category_label" + if (aHasCategoryLabel && !bHasCategoryLabel) { + return -1 + } else if (!aHasCategoryLabel && bHasCategoryLabel) { + return 1 + } else { + return 0 + } + } // Function to deduplicate based on the "name" field const deduplicateByName = (array) => { - const uniqueNames = {}; - return array.filter(item => { - if (!item.hasOwnProperty('name') || !item.name.length) { - return true - } - if (!uniqueNames[item.name]) { - uniqueNames[item.name] = true - return true - } - return false - }) + const uniqueNames = {}; + return array.filter(item => { + if (!item.hasOwnProperty('name') || !item.name.length) { + return true + } + if (!uniqueNames[item.name]) { + uniqueNames[item.name] = true + return true + } + return false + }) } - // Gets the most important actions first - const renderedActionOptions = deduplicateByName(( - selectedApp.actions === undefined || selectedApp.actions === null ? [] : - selectedApp.actions.filter((a) => - a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label")) - ).sort(sortByCategoryLabel)) - + // Gets the most important actions first + const renderedActionOptions = deduplicateByName(( + selectedApp.actions === undefined || selectedApp.actions === null ? [] : + selectedApp.actions.filter((a) => + a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label")) + ).sort(sortByCategoryLabel)) - const selectedAppIcon = selectedAction.large_image - var newAppname = selectedAction?.name?.charAt(0).toUpperCase() + selectedAction?.name?.substring(1) - if (newAppname === undefined || newAppname === null) { - newAppname = "" - } else { - newAppname = newAppname.replaceAll("_", " ") - } + const selectedAppIcon = selectedAction.large_image - return ( -
+ var newAppname = selectedAction?.name?.charAt(0).toUpperCase() + selectedAction?.name?.substring(1) + if (newAppname === undefined || newAppname === null) { + newAppname = "" + } else { + try { + newAppname = newAppname?.replaceAll("_", " ") + } catch (e) { + console.log("Error in replace newappname: ", e) + } - {hideExtraTypes === true ? null : ( - -
-
-
{ - //window.open("/apps/${selectedAction.app_id}", "_blank") - }} - > - - - + } -

- {newAppname} -

-
-
- { - if (workflowExecutions.length > 0) { - // Look for the ID - var found = false; - for (let [key,keyval] in Object.entries(workflowExecutions)) { - if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { - continue; - } + var optionalFound = false + return ( +
- var foundResult = workflowExecutions[key].results.find( - (result) => result.action.id === selectedAction.id - ) + {hideExtraTypes === true ? null : ( + +
+
+
{ + //window.open("/apps/${selectedAction.app_id}", "_blank") + }} + > + + + + + - if (foundResult === undefined || foundResult === null) { - continue - } +

+ {newAppname} +

+
+
+ { + if (workflowExecutions.length > 0) { + // Look for the ID + var found = false; + for (let [key, keyval] in Object.entries(workflowExecutions)) { + if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { + continue; + } - const oldstartnode = cy.getElementById(selectedAction.id); - if (oldstartnode !== undefined && oldstartnode !== null) { - const foundname = oldstartnode.data("label") - if (foundname !== undefined && foundname !== null) { - foundResult.action.label = foundname - } - } + var foundResult = workflowExecutions[key].results.find( + (result) => result.action.id === selectedAction.id + ) - setSelectedResult(foundResult); - if (setCodeModalOpen !== undefined) { - setCodeModalOpen(true) - - found = true - } + if (foundResult === undefined || foundResult === null) { + continue + } - break - } + const oldstartnode = cy.getElementById(selectedAction.id); + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + foundResult.action.label = foundname + } + } - if (!found) { - toast("No result for this action yet. Please run the workflow first.") - } - } - }} - > - - - - - { - setAuthenticationModalOpen(true) - }} - > - - - - + setSelectedResult(foundResult); + if (setCodeModalOpen !== undefined) { + setCodeModalOpen(true) - { - if (setAiQueryModalOpen !== undefined) { - setAiQueryModalOpen(true) - } else { - aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) - } - - setAutocompleting(true) - setTimeout(() => { - setAutocompleting(false) - }, 3000) - }} - > - - {autoCompleting ? - - : - - } - - -
-
-
- {selectedApp.versions !== null && - selectedApp.versions !== undefined && - selectedApp.versions.length > 1 ? ( - - ) : null} -
-
-
-
- Name + { + if (setAiQueryModalOpen !== undefined) { + setAiQueryModalOpen(true) + } else { + aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) + } + + setAutocompleting(true) + setTimeout(() => { + setAutocompleting(false) + }, 3000) + }} + > + + {autoCompleting ? + + : + + } + + + + {(selectedAction?.generated === true && selectedAction?.app_version === "1.0.0") || (selectedAction?.app_name === "Shuffle Tools" && selectedAction?.app_version !== "1.2.0") ? + + : null} + +
+
+
+ {selectedApp.versions !== null && + selectedApp.versions !== undefined && + selectedApp.versions.length > 1 ? ( + + ) : null} +
+
+
+
+ Name { }} fullWidth color="primary" + disabled={selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0} placeholder={selectedAction.label} value={appActionName} - onChange={ - (event) => { - let newValue = event.target.value - newValue = newValue.replaceAll(" ", "_") - setAppActionName(newValue) - } - } + onChange={(event) => { + let newValue = event.target.value + newValue = newValue.replaceAll(" ", "_") + setAppActionName(newValue) + }} onBlur={(e) => { // Copy the name value const name = e.target.value - const parsedBaseLabel = "$"+prevActionName.toLowerCase().replaceAll(" ", "_") - const newname = "$"+name.toLowerCase().replaceAll(" ", "_") + const parsedBaseLabel = "$" + prevActionName.toLowerCase().replaceAll(" ", "_") + const newname = "$" + name.toLowerCase().replaceAll(" ", "_") // Check if it's the same as the current name in use //if (name === selectedAction.label) { @@ -1728,10 +1933,10 @@ const ParsedAction = (props) => { // Change in actions, triggers & conditions // Highlight the changes somehow with a glow? - if (workflow.branches !== undefined && workflow.branches !== null) { - for (let [key,keyval] in Object.entries(workflow.branches)) { + if (workflow.branches !== undefined && workflow.branches !== null) { + for (let [key, keyval] in Object.entries(workflow.branches)) { if (workflow.branches[key].conditions !== undefined && workflow.branches[key].conditions !== null) { - for (let [subkey,subkeyval] in Object.entries(workflow.branches[key].conditions)) { + for (let [subkey, subkeyval] in Object.entries(workflow.branches[key].conditions)) { const condition = workflow.branches[key].conditions[subkey] const sourceparam = condition.source const destinationparam = condition.destination @@ -1743,46 +1948,46 @@ const ParsedAction = (props) => { var cnt = -1 var previous = 0 while (true) { - cnt += 1 + cnt += 1 // Need to make sure e.g. changing the first here doesn't change the 2nd // $change_me // $change_me_2 - + const foundindex = sourceparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) if (foundindex === previous && foundindex !== 0) { break } - + if (foundindex >= 0) { - previous = foundindex+newname.length + previous = foundindex + newname.length // Need to add diff of length to word - + // Check location: // If it's a-zA-Z_ then don't replace - if (sourceparam.value.length > foundindex+parsedBaseLabel.length) { + if (sourceparam.value.length > foundindex + parsedBaseLabel.length) { const regex = /[a-zA-Z0-9_]/g; - const match = sourceparam.value[foundindex+parsedBaseLabel.length].match(regex); + const match = sourceparam.value[foundindex + parsedBaseLabel.length].match(regex); if (match !== null) { continue } } - + console.log("Old found: ", workflow.branches[key].conditions[subkey].source.value) - const extralength = newname.length-parsedBaseLabel.length - sourceparam.value = sourceparam.value.substring(0, foundindex) + newname + sourceparam.value.substring(foundindex-extralength+newname.length, sourceparam.value.length) + const extralength = newname.length - parsedBaseLabel.length + sourceparam.value = sourceparam.value.substring(0, foundindex) + newname + sourceparam.value.substring(foundindex - extralength + newname.length, sourceparam.value.length) console.log("New: ", workflow.branches[key].conditions[subkey].source.value) - } else { + } else { break } - + // Break no matter what after 5 replaces. May need to increase if (cnt >= 5) { break } - + } - } catch (e) { + } catch (e) { console.log("Failed value replacement based on index: ", e) } } @@ -1792,46 +1997,46 @@ const ParsedAction = (props) => { var cnt = -1 var previous = 0 while (true) { - cnt += 1 + cnt += 1 // Need to make sure e.g. changing the first here doesn't change the 2nd // $change_me // $change_me_2 - + const foundindex = destinationparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) if (foundindex === previous && foundindex !== 0) { break } - + if (foundindex >= 0) { - previous = foundindex+newname.length + previous = foundindex + newname.length // Need to add diff of length to word - + // Check location: // If it's a-zA-Z_ then don't replace - if (destinationparam.value.length > foundindex+parsedBaseLabel.length) { + if (destinationparam.value.length > foundindex + parsedBaseLabel.length) { const regex = /[a-zA-Z0-9_]/g; - const match = destinationparam.value[foundindex+parsedBaseLabel.length].match(regex); + const match = destinationparam.value[foundindex + parsedBaseLabel.length].match(regex); if (match !== null) { continue } } - + console.log("Old found: ", workflow.branches[key].conditions[subkey].destination.value) - const extralength = newname.length-parsedBaseLabel.length - destinationparam.value = destinationparam.value.substring(0, foundindex) + newname + destinationparam.value.substring(foundindex-extralength+newname.length, destinationparam.value.length) + const extralength = newname.length - parsedBaseLabel.length + destinationparam.value = destinationparam.value.substring(0, foundindex) + newname + destinationparam.value.substring(foundindex - extralength + newname.length, destinationparam.value.length) console.log("New: ", workflow.branches[key].conditions[subkey].destination.value) - } else { + } else { break } - + // Break no matter what after 5 replaces. May need to increase if (cnt >= 5) { break } - + } - } catch (e) { + } catch (e) { console.log("Failed value replacement based on index: ", e) } } @@ -1840,7 +2045,7 @@ const ParsedAction = (props) => { } } - for (let [key,keyval] in Object.entries(workflow.actions)) { + for (let [key, keyval] in Object.entries(workflow.actions)) { if (workflow.actions[key].id === selectedAction.id) { continue } @@ -1865,7 +2070,7 @@ const ParsedAction = (props) => { var cnt = -1 var previous = 0 while (true) { - cnt += 1 + cnt += 1 // Need to make sure e.g. changing the first here doesn't change the 2nd // $change_me // $change_me_2 @@ -1873,302 +2078,382 @@ const ParsedAction = (props) => { if (foundindex === previous && foundindex !== 0) { break } - + if (foundindex >= 0) { - previous = foundindex+newname.length + previous = foundindex + newname.length // Need to add diff of length to word - + // Check location: // If it's a-zA-Z_ then don't replace - if (param.value.length > foundindex+parsedBaseLabel.length) { + if (param.value.length > foundindex + parsedBaseLabel.length) { const regex = /[a-zA-Z0-9_]/g; - const match = param.value[foundindex+parsedBaseLabel.length].match(regex); + const match = param.value[foundindex + parsedBaseLabel.length].match(regex); if (match !== null) { continue } } - - const extralength = newname.length-parsedBaseLabel.length - param.value = param.value.substring(0, foundindex) + newname + param.value.substring(foundindex-extralength+newname.length, param.value.length) - } else { + const extralength = newname.length - parsedBaseLabel.length + param.value = param.value.substring(0, foundindex) + newname + param.value.substring(foundindex - extralength + newname.length, param.value.length) + + } else { break } - + // Break no matter what after 5 replaces. May need to increase if (cnt >= 5) { break } - + } - } catch (e) { + } catch (e) { console.log("Failed value replacement based on index: ", e) } } } - setWorkflow(workflow); - setUpdate(Math.random()); + setWorkflow(workflow) + setUpdate(Math.random()) setPrevActionName(name) }} />
{/*!isCloud ? null :*/} -
- - - Delay - { - setDelay(event.target.value) - }} - /> - - -
+
+ + + Delay + 0} + placeholder={selectedAction.execution_delay} + value={delay} + onChange={(event) => { + setDelay(event.target.value) + }} + /> + + +
{/**/}
- - )} + + )} - {selectedApp.name !== undefined && - selectedAction.authentication !== null && - selectedAction.authentication !== undefined && - selectedAction.authentication.length === 0 && - requiresAuthentication ? ( -
- - - - - -
- ) : null} + setAuthenticationModalOpen(true); + }} + > + Authenticate{" "} + {selectedApp.name.replaceAll("_", " ")} + + + +
+ ) : null} - {selectedAction.authentication !== undefined && + {selectedAction.authentication !== undefined && selectedAction.authentication !== null && selectedAction.authentication.length > 0 ? ( -
- Authentication -
- { - if (selectedAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app") { - } else { - selectedAction.parameters[key].value = "authgroup controlled" - } - } - } + if (e.target.value === "No selection") { + selectedAction.selectedAuthentication = {}; + selectedAction.authentication_id = ""; - setSelectedAction(selectedAction) - setUpdate(Math.random()) - } - } else { - selectedAction.selectedAuthentication = e.target.value; - selectedAction.authentication_id = e.target.value.id; - setSelectedAction(selectedAction) - setUpdate(Math.random()) - } - }} - style={{ - backgroundColor: theme.palette.inputColor, - color: "white", - height: 35, - maxWidth: rightsidebarStyle.maxWidth - 80, - borderRadius: theme.palette?.borderRadius, - }} - > - - No selection - - {selectedAction.authentication.map((data) => { - if (data.last_modified === true) { - //console.log("LAST MODIFIED: ", data.label) - } + for (let [key, keyval] in Object.entries(selectedAction.parameters)) { + if (selectedAction.parameters[key].configuration === false) { + //console.log("FIELDSKIP: ", selectedAction.parameters[key].name) + continue + } - return ( - + if (selectedAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app" && selectedAction.parameters[key].value.includes("http")) { + continue + } - {data?.validation?.valid === true ? - + if (selectedAction.parameters[key].example !== undefined && selectedAction.parameters[key].example !== null && selectedAction.parameters[key].example !== "") { + if (selectedAction.parameters[key].example.toLowerCase().includes("apik") || selectedAction.parameters[key].example.toLowerCase().includes("key") || selectedAction.parameters[key].example.toLowerCase().includes("pass") || selectedAction.parameters[key].example.toLowerCase().includes("****")) { + selectedAction.parameters[key].value = "" + } else { + selectedAction.parameters[key].value = selectedAction.parameters[key].example + } + + } else { + selectedAction.parameters[key].value = "" + } + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + } else if (e.target.value === "authgroups") { + if (authGroups !== undefined && authGroups !== null && authGroups.length === 0) { + toast("No auth groups created. Opening window to create one") + + setTimeout(() => { + window.open("/admin?tab=app_auth", "_blank") + }, 2500) + } else { + selectedAction.selectedAuthentication = {}; + selectedAction.authentication_id = "authgroups" + + for (let [key, keyval] in Object.entries(selectedAction.parameters)) { + //console.log(selectedAction.parameters[key]) + if (selectedAction.parameters[key].configuration) { + + if (selectedAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app") { + } else { + selectedAction.parameters[key].value = "authgroup controlled" + } + } + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + } + } else { + selectedAction.selectedAuthentication = e.target.value; + selectedAction.authentication_id = e.target.value.id; + + setDistributeAuthToSuborgs(e.target.value?.suborg_distributed || false) + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + } + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + height: 35, + maxWidth: rightsidebarStyle.maxWidth - 80, + borderRadius: theme.palette?.borderRadius, + }} + > + + No selection + + {selectedAction.authentication.map((data) => { + if (data.last_modified === true) { + //console.log("LAST MODIFIED: ", data.label) + } + + return ( + + + {data?.validation?.valid === true ? + + + + : null} + {data?.last_modified === true ? + + : null} + {/*data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ? - - : null } - {data?.last_modified === true ? - - : null} - {/*data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ? - - : null*/} - {data.label} - - ); - })} + : null*/} + {data.label} + + ); + })} - + - - Auth Groups - + + Auth Groups + - + - - { - setAuthenticationModalOpen(true); - }} - > - - - -
-
- ) : null} + + { + setAuthenticationModalOpen(true); + }} + > + + + +
+ +
+ ) : null} - {selectedAction.authentication_id === "authgroups" && (authGroups === undefined || authGroups === null || authGroups.length === 0) ? - - - Create your first Authentication group - - - : null} + {selectedAction.authentication_id === "authgroups" && (authGroups === undefined || authGroups === null || authGroups.length === 0) ? + + + Create your first Authentication group + + + : null} - {/*showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? ( + {/*showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? (
Environment 0 - ? selectedAction.execution_variable.name - : "No selection" - } - SelectDisplayProps={{ - style: { - }, - }} - fullWidth - onChange={(e) => { - if (e.target.value === "No selection") { - selectedAction.execution_variable = { name: "No selection" }; - } else { - const value = workflow.execution_variables.find( - (a) => a.name === e.target.value - ); - selectedAction.execution_variable = value; - } - setSelectedAction(selectedAction); - setUpdate(Math.random()); - }} - style={{ - backgroundColor: theme.palette.inputColor, - color: "white", - height: "50px", - borderRadius: theme.palette?.borderRadius, - }} - > - - No selection - - - {workflow.execution_variables.map((data) => ( - - {data.name} - - ))} - -
- ) : null} + {workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ? ( +
+ Execution variable (optional) + +
+ ) : null} - -
- {/*hideExtraTypes ? null : + +
+ {/*hideExtraTypes ? null :
Actions
*/} - {setNewSelectedAction !== undefined ? ( - { - // FIXME: Sorting - // Most popular - // Is categorized - // Uncategorized - return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; - }} - renderGroup={(params) => { + {isAgent ? null : + setNewSelectedAction !== undefined ? ( + 0)} + autoHighlight + value={selectedAction} + classes={{ inputRoot: classes.inputRoot }} + groupBy={(option) => { + // FIXME: Sorting + // Most popular + // Is categorized + // Uncategorized + return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; + }} + renderGroup={(params) => { - return ( -
  • - {params.group} - {params.children} -
  • - ) - }} - options={renderedActionOptions} - ListboxProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - }, - }} - filterOptions={(options, { inputValue }) => { - const lowercaseValue = inputValue.toLowerCase() - options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) - - return options - }} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.name === undefined || option.name === null ) { - return null; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - - return newname; - }} - fullWidth - sx={{ - '& .MuiOutlinedInput-root': { - height: 40, // Adjust the input height - }, - '& .MuiAutocomplete-input': { - padding: '8px', // Adjust the text padding - }, - }} - - style={{ - backgroundColor: theme.palette.backgroundColor, - height: 35, - borderRadius: theme.palette?.borderRadius, - }} - onChange={(event, newValue) => { - // Workaround with event lol - if (newValue !== undefined && newValue !== null) { - setNewSelectedAction({ - target: { - value: newValue.name - } - }); - } - }} - renderOption={(props, option, state) => { - var newActionname = option.name; - if (option.label !== undefined && option.label !== null && option.label.length > 0) { - newActionname = option.label; - } - - var newActiondescription = option.description; - //console.log("DESC: ", newActiondescription) - if (option.description === undefined || option.description === null) { - newActiondescription = "Description: No description defined for this action" - } else { - newActiondescription = "Description: "+newActiondescription - } - - const iconInfo = GetIconInfo({ name: option.name }); - const useIcon = iconInfo.originalIcon; - - if (newActionname === undefined || newActionname === null) { - newActionname = "No name" - option.name = "No name" - option.label = "No name" - } - - newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); - - var method = "" - var extraDescription = "" - if (option.name.includes("get_")) { - method = "GET" - } else if (option.name.includes("post_")) { - method = "POST" - } else if (option.name.includes("put_")) { - method = "PUT" - } else if (option.name.includes("patch_")) { - method = "PATCH" - } else if (option.name.includes("delete_")) { - method = "DELETE" - } else if (option.name.includes("options_")) { - method = "OPTIONS" - } else if (option.name.includes("connect_")) { - method = "CONNECT" - } - - // FIXME: Should it require a base URL? - if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) { - var extraUrl = "" - const descSplit = option.description.split("\n") - // Last line of descSplit - if (descSplit.length > 0) { - extraUrl = descSplit[descSplit.length-1] - } - - if (extraUrl.length > 0) { - if (extraUrl.includes(" ")) { - extraUrl = extraUrl.split(" ")[0] - } - - if (extraUrl.includes("#")) { - extraUrl = extraUrl.split("#")[0] - } - - extraDescription = `${method} ${extraUrl}` - } else { - //console.log("No url found. Check again :)") - } - } - - return ( - - ); - }} - renderInput={(params) => { - if (params.inputProps?.value) { - const prefixes = ["Post", "Put", "Patch"]; - for (let prefix of prefixes) { - if (params.inputProps.value.startsWith(prefix)) { - let newValue = params.inputProps.value.replace(prefix + " ", ""); - if (newValue.length > 1) { - newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1); - } - // Set the new value without mutating inputProps - params = { ...params, inputProps: { ...params.inputProps, value: newValue } }; - break; - } - } - // Check if it starts with "Get List" and method is "Get" - if (params.inputProps.value.startsWith("Get List")) { - console.log("Get List"); - } - } - - - const actionDescription = null - - return ( - + {params.group} + {params.children} + + ) + }} + options={renderedActionOptions} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", }, }} - > - { + const lowercaseValue = inputValue.toLowerCase() + options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) - data-lpignore="true" - autocomplete="off" - dataLPIgnore="true" - autoComplete="off" - - color="primary" - id="checkbox-search" - variant="body1" - style={theme.palette.textFieldStyle} - label={isIntegration ? "Choose a category" : "Find Actions"} - variant="outlined" - name={`disable_autocomplete_${Math.random()}`} - /> - - ); - }} - /> - ) : null} - -
    { - selectedActionParameters !== undefined && selectedActionParameters !== null && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ? -
    - {isIntegration ? - apps !== undefined && apps !== null && apps.length > 0 ? -
    -
    { - - selectedAction.example = "noapp" - selectedAction.large_image = newimage - if (cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(selectedAction.id) - if (foundnode !== undefined && foundnode !== null) { - foundnode.data("large_image", newimage) - } + return options + }} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.name === undefined || option.name === null) { + return null; } - const iconInfo = GetIconInfo(selectedAction) - if (iconInfo !== undefined && iconInfo !== null) { - selectedAction.fillGradient = iconInfo.fillGradient - - selectedAction.iconBackground = iconInfo.iconBackgroundColor - selectedAction.fillstyle = "linear-gradient" + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + + return newname; + }} + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + height: 40, // Adjust the input height + }, + '& .MuiAutocomplete-input': { + padding: '8px', // Adjust the text padding + }, + }} + + style={{ + backgroundColor: theme.palette.backgroundColor, + height: 35, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + // Workaround with event lol + if (newValue !== undefined && newValue !== null) { + setNewSelectedAction({ + target: { + value: newValue.name + } + }); + } + }} + renderOption={(props, option, state) => { + var newActionname = option.name; + if (option.label !== undefined && option.label !== null && option.label.length > 0) { + newActionname = option.label; } - if (paramIndex === -1) { - console.log("Couldn't find app_name parameter") - selectedAction.parameters.push({ - name: "app_name", - value: wrapperapp.name, - autocompleted: false, - }) + var newActiondescription = option.description; + //console.log("DESC: ", newActiondescription) + if (option.description === undefined || option.description === null) { + newActiondescription = "Description: No description defined for this action" } else { - selectedAction.parameters[paramIndex].value = wrapperapp.name - } - - setSelectedAction(selectedAction) - setUpdate(Math.random()) - - }}> - -
    - -
    -
    -
    - {apps.map((app, appIndex) => { - if (app.categories === undefined || app.categories === null || app.categories.length === 0) { - return null + newActiondescription = "Description: " + newActiondescription } - var found = false + const iconInfo = GetIconInfo({ name: option.name }); + const useIcon = iconInfo.originalIcon; + if (newActionname === undefined || newActionname === null) { + newActionname = "No name" + option.name = "No name" + option.label = "No name" + } - for (var key in app.categories) { - if (app.categories[key].toLowerCase() !== actionname) { - continue + newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); + + var method = "" + var extraDescription = "" + if (option.name.includes("get_")) { + method = "GET" + } else if (option.name.includes("post_")) { + method = "POST" + } else if (option.name.includes("put_")) { + method = "PUT" + } else if (option.name.includes("patch_")) { + method = "PATCH" + } else if (option.name.includes("delete_")) { + method = "DELETE" + } else if (option.name.includes("options_")) { + method = "OPTIONS" + } else if (option.name.includes("connect_")) { + method = "CONNECT" + } + + // FIXME: Should it require a base URL? + if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) { + var extraUrl = "" + const descSplit = option.description.split("\n") + // Last line of descSplit + if (descSplit.length > 0) { + extraUrl = descSplit[descSplit.length - 1] } - found = true - break - } + if (extraUrl.length > 0) { + if (extraUrl.includes(" ")) { + extraUrl = extraUrl.split(" ")[0] + } - if (!found) { - return null - } + if (extraUrl.includes("#")) { + extraUrl = extraUrl.split("#")[0] + } - var isAppSelected = false - const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") - if (paramIndex > -1) { - // Check the actual value and if it's the same - if (selectedAction.parameters[paramIndex].value === app.name) { - isAppSelected = true + extraDescription = `${method} ${extraUrl}` + } else { + //console.log("No url found. Check again :)") } } return ( -
    { - selectedAction.example = "" - selectedAction.large_image = app.large_image - if (cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(selectedAction.id) - if (foundnode !== undefined && foundnode !== null) { - foundnode.data("large_image", app.large_image) - } - } - - if (paramIndex === -1) { - console.log("Couldn't find app_name parameter") - selectedAction.parameters.push({ - name: "app_name", - value: app.name, - autocompleted: false, - }) - } else { - selectedAction.parameters[paramIndex].value = app.name + + ); + }} + renderInput={(params) => { + if (params.inputProps?.value) { + const prefixes = ["Post", "Put", "Patch"]; + for (let prefix of prefixes) { + if (params.inputProps.value.startsWith(prefix)) { + let newValue = params.inputProps.value.replace(prefix + " ", ""); + if (newValue.length > 1) { + newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1); + } + // Set the new value without mutating inputProps + params = { ...params, inputProps: { ...params.inputProps, value: newValue } }; + break; } - - setSelectedAction(selectedAction) - setUpdate(Math.random()) + } + // Check if it starts with "Get List" and method is "Get" + if (params.inputProps.value.startsWith("Get List")) { + console.log("Get List"); + } + } - }}> - - - -
    - ) - })} -
    - : null - : - -
    - {/* + + const actionDescription = null + + return ( + + 0)} + /> + + ); + }} + /> + ) : null} + + {selectedAction?.app_name === "Shuffle AI" && selectedAction?.name === "run_llm" ? + selectedAction?.environment === "Cloud" && isCloud ? ( + + Info: Cloud Inference processing runs with Shuffle's GPUs in EU, Netherlands, and may be unstable. Your data is NOT stored there. + + ) + : + + This action is slow without GPU's. Use Shuffle's Cloud Runtime location for faster processing. + + : + null + } + +
    { + selectedActionParameters !== undefined && selectedActionParameters !== null && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ? +
    + {isIntegration || isAgent ? + apps !== undefined && apps !== null && apps.length > 0 ? +
    +
    { + + selectedAction.example = "noapp" + selectedAction.large_image = newimage + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", newimage) + } + } + + /* + const iconInfo = GetIconInfo(selectedAction) + if (iconInfo !== undefined && iconInfo !== null) { + selectedAction.fillGradient = iconInfo.fillGradient + + selectedAction.iconBackground = iconInfo.iconBackgroundColor + selectedAction.fillstyle = "linear-gradient" + } + */ + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: wrapperapp.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = wrapperapp.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + +
    + +
    +
    +
    + + {apps.map((app, appIndex) => { + if (app.categories === undefined || app.categories === null || app.categories.length === 0) { + return null + } + + var newactionname = actionname.toLowerCase() + if (isAgent === true) { + newactionname = "ai" + } + + var found = false + for (var key in app.categories) { + if (app.categories[key].toLowerCase() !== newactionname) { + continue + } + + found = true + break + } + + if (!found) { + return null + } + + var isAppSelected = false + const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex > -1) { + // Check the actual value and if it's the same + if (selectedAction.parameters[paramIndex].value === app.name) { + isAppSelected = true + } + } + + return ( +
    { + selectedAction.example = "" + selectedAction.large_image = app.large_image + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: app.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = app.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + + + +
    + ) + })} +
    + : null + : + +
    + {/* */} - - } + + } - {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? -
    - - Select an app you want to use - - { - console.log("LABEL: ", option) - if ( - option === undefined || - option === null || - option.app_name === undefined || - option.app_name === null - ) { - return null; - } + {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? +
    + + Select an app you want to use + + { + console.log("LABEL: ", option) + if ( + option === undefined || + option === null || + option.app_name === undefined || + option.app_name === null + ) { + return null; + } - const newname = ( - option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={selectedAction.matching_actions} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 35, - borderRadius: theme.palette?.borderRadius, - }} - onChange={(event, newValue) => { - console.log("SELECT: ", event, newValue) - // Workaround with event lol - //if (newValue !== undefined && newValue !== null) { - // setNewSelectedAction({ target: { value: newValue.name } }); - //} - }} - renderOption={(props, data, state) => { - var newActionname = data.app_name; - if ( - data.label !== undefined && - data.label !== null && - data.label.length > 0 - ) { - newActionname = data.label; - } + const newname = ( + option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={selectedAction.matching_actions} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 35, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + console.log("SELECT: ", event, newValue) + // Workaround with event lol + //if (newValue !== undefined && newValue !== null) { + // setNewSelectedAction({ target: { value: newValue.name } }); + //} + }} + renderOption={(props, data, state) => { + var newActionname = data.app_name; + if ( + data.label !== undefined && + data.label !== null && + data.label.length > 0 + ) { + newActionname = data.label; + } - const iconInfo = GetIconInfo({ name: data.app_name }); - const useIcon = iconInfo.originalIcon; + const iconInfo = GetIconInfo({ name: data.app_name }); + const useIcon = iconInfo.originalIcon; - console.log("Actionname 1: ", newActionname) + console.log("Actionname 1: ", newActionname) - newActionname = ( - newActionname.charAt(0).toUpperCase() + - newActionname.substring(1) - ).replaceAll("_", " "); + newActionname = ( + newActionname.charAt(0).toUpperCase() + + newActionname.substring(1) + ).replaceAll("_", " "); - return ( -
    - + + {useIcon} + + {newActionname} +
    + ); + }} + renderInput={(params) => { + if (params.inputProps !== undefined && params.inputProps !== null && params.inputProps.value !== undefined && params.inputProps.value !== null) { + const prefixes = ["Post", "Put", "Patch"] + for (let [key, keyval] in Object.entries(prefixes)) { + if (params.inputProps.value.startsWith(prefixes[key])) { + params.inputProps.value = params.inputProps.value.replace(prefixes[key] + " ", "", -1) + if (params.inputProps.value.length > 1) { + params.inputProps.value = params.inputProps.value.charAt(0).toUpperCase() + params.inputProps.value.substring(1) + } + break + } + } + } + + return ( + + ); + }} + /> +
    + : null} + {selectedAction.description !== undefined && selectedAction.description !== null && selectedAction.description.length > 0 && hiddenParameters === false ? ( +
    + + Description + + + {selectedAction.description} + +
    + ) : null} + + {suggestionInfo()} + {selectedActionParameters?.map((data, count) => { + if (data.variant === "") { + data.variant = "STATIC_VALUE"; + } + + if ((isIntegration || isAgent) && data.name === "app_name") { + return null + } + + /* + // Somehow autogenerate from the app itself + if (isAgent && data.name == "model") { + //console.log("Options: ", data.options) + } + */ + + if (data.value === "authgroup controlled") { + if (data?.name === "url" && authenticationType?.type === "oauth2-app") { + } else { + return null + } + } + + if (selectedAction.parameters === undefined || selectedAction.parameters === null || selectedAction.parameters.length !== selectedActionParameters.length) { + + //selectedAction.parameters = selectedActionParameters + //console.log("PARAM BUG - length change(?): ", selectedAction) + } + + //!selectedAction.auth_not_required && + if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { + + // This sets the placeholder in the frontend. (Replaced in backend) + if (selectedActionParameters[count] !== undefined) { + selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + + if (selectedAction.parameters[count] !== undefined) { + selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + + setSelectedAction(selectedAction); + //setUpdate(Math.random()) + + if (authWritten) { + return null; + } + + authWritten = true; + return null + + /* + // FIXME: Is this part necessary to show? + return ( + + Authentication fields are hidden + + ); + */ + } + + + // Added autofill to make this ALOT simpler + if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { + if (selectedActionParameters[count].length === 0) { + selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" + setSelectedAction(selectedAction) + } + + return null + } + + var staticcolor = "inherit"; + var actioncolor = "inherit"; + var varcolor = "inherit"; + var multiline + if ( + data.multiline !== undefined && + data.multiline !== null && + data.multiline === true + ) { + multiline = true; + } + + // make data.value from array to comma separated string if it is an array + if (data.value !== undefined && data.value !== null && Array.isArray(data.value)) { + data.value = data.value.join(",") + } + + if (data.value !== undefined && data.value !== null && + data.value.startsWith("{") && data.value.endsWith("}")) { + multiline = true + } + + var placeholder = "Value"; + if (data.example !== undefined && data.example !== null && data.example.length > 0) { + + placeholder = data.example; + + // if (data.name === "url") { + // data.value = data.example; + // } + // In case of data.example + if (data.value === undefined || data.value === null) { + data.value = "" + } + + if (data.value.length === 0) { + if (data.name.toLowerCase() === "headers") { + //console.log("Should show headers field instead with + and -!") + + // Check if file ID exists + // + const fileFound = selectedActionParameters.find(param => param.name === "file_id") + if (fileFound === undefined || fileFound === null) { + data.value = data.example + } else { + // Purposely unset it if set by default when using files + data.value = "" + } + } + } + + /* + if (data.name !== "queries" && data.name !== "key" && data.name !== "value" ) { + data.value = data.example + } + } + */ + } + + if (selectedAction.name === "custom_action" && data.name === "body") { + for (var key in selectedActionParameters) { + const param = selectedActionParameters[key] + if (param.name === "method") { + if (param.value === "GET") { + return null + } + } + } + } + + if (data.name.startsWith("${") && data.name.endsWith("}")) { + const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); + + + if (paramcheck !== undefined && paramcheck !== null) { + if ( + paramcheck["value_replace"] !== undefined && + paramcheck["value_replace"] !== null + ) { + //console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"]) + const subparamindex = paramcheck["value_replace"].findIndex( + (param) => param.key === data.name + ); + if (subparamindex !== -1) { + data.value = + paramcheck["value_replace"][subparamindex]["value"]; + } + } + } + } + + + var showCacheConfig = false + if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") { + // Show a key popout button + showCacheConfig = true + } + + var disabled = false; + var rows = "3"; + var openApiHelperText = "This is an OpenAPI specific field"; + + + if (selectedApp.generated && data.name === "headers") { + //console.log("HEADER: ", data) + //if (data.value.length === 0) { + //} + //setSelectedActionParameters(selectedActionParameters) + } + + var hideBodyButtonValue = ( +
    + - {useIcon} - - {newActionname} + { + // Set localstorage + localStorage.setItem("hideBody", "true") + + setHideBody(false) + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "UNTOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + // Check required fields here + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { + // Look for the field name in the required_body_fields + if (selectedAction.required_body_fields.includes(param.name)) { + param.required = true + } else { + param.required = false + } + } + + return { ...param, field_active: true } + } + + return param + }) + + setSelectedActionParameters(updatedParameters) + }} + /> + { + localStorage.setItem("hideBody", "false") + setHideBody(true) + // Make sure the body field is shown + var foundvalue = false + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "TOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + if (param.value.length > 0) { + foundvalue = true + } + + return { ...param, field_active: false } + } + + return param + }) + + if (foundvalue === true) { + toast.info("Please fill either Simple fields OR Advanced body, not both") + } + + setSelectedActionParameters(updatedParameters) + }} + /> +
    - ); - }} - renderInput={(params) => { - if (params.inputProps !== undefined && params.inputProps !== null && params.inputProps.value !== undefined && params.inputProps.value !== null) { - const prefixes = ["Post", "Put", "Patch"] - for (let [key,keyval] in Object.entries(prefixes)) { - if (params.inputProps.value.startsWith(prefixes[key])) { - params.inputProps.value = params.inputProps.value.replace(prefixes[key]+" ", "", -1) - if (params.inputProps.value.length > 1) { - params.inputProps.value = params.inputProps.value.charAt(0).toUpperCase()+params.inputProps.value.substring(1) + ) + + var showButtonField = false + if (selectedApp.generated === true && data.name === "body") { + const regex = /\${(\w+)}/g; + const found = placeholder.match(regex); + + var newhidebody = hideBody + showButtonField = true + if (found === undefined || found === null || found.length === 0) { + newhidebody = false + hideBodyButtonValue = null + + if (hideBody === false) { + setHideBody(true) + } + } + + if (newhidebody === true) { + //toast("BODYBUTTON TRUE") + } else { + + rows = "1"; + disabled = true; + openApiHelperText = "OpenAPI spec: fill the following fields."; + + var changed = false; + var tempArray = [] + for (let specKey in found) { + const tmpitem = found[specKey]; + var skip = false; + + for (let innerkey in selectedActionParameters) { + if (selectedActionParameters[innerkey].name === tmpitem) { + skip = true; + break; + } } + + if (skip) { + //console.log("SKIPPING ", tmpitem) + continue; + } + + changed = true; + var isRequired = false + // Check if original field name is in the selectedAction.required_body_fields + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null) { + for (let innerkey in selectedAction.required_body_fields) { + if (selectedAction.required_body_fields[innerkey] === tmpitem) { + isRequired = true + break + } + } + } + + tempArray.push({ + action_field: "", + configuration: false, + description: openApiFieldDesc, + example: "", + id: "", + multiline: false, + name: tmpitem, + options: null, + required: isRequired, + schema: { type: "string" }, + skip_multicheck: false, + tags: null, + value: "", + variant: "STATIC_VALUE", + field_active: true, + + autocompleted: false, + }); + } + + var required = selectedActionParameters.filter(item => item.required === true) + var notRequired = selectedActionParameters.filter(item => item.required === false) + + if (tempArray.length > 0) { + // Sort tempArray based on tempArray.required + tempArray.sort((a, b) => (a.required < b.required) ? 1 : -1) + // Add all items to the selectedActionParameters array + for (let innerkey in tempArray) { + tempArray[innerkey].id = "ADDED" + + if (tempArray[innerkey].required === true) { + required.push(tempArray[innerkey]) + } else { + notRequired.push(tempArray[innerkey]) + } + } + } + + if (changed) { + // Sort selectedActionParameters based on selectedActionParameters.required + // Find the "headers" and "queries" field names and put them on the first indexes anyway + var newArray = required.concat(notRequired) + + + setSelectedActionParameters(newArray) + } + + } + } + + const clickedFieldId = "rightside_field_" + count; + + var baseHelperText = "" + if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) { + baseHelperText = calculateHelpertext(data.value) + } + + var tmpitem = data.name.valueOf(); + if (data.name.startsWith("${") && data.name.endsWith("}")) { + tmpitem = tmpitem.slice(2, data.name.length - 1); + } + + if (tmpitem === "from_shuffle") { + tmpitem = "from" + } + + tmpitem = ( + tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1) + ).replaceAll("_", " "); + + if (tmpitem === "Username basic") { + tmpitem = "Username" + } else if (tmpitem === "Password basic") { + tmpitem = "Password" + } + + // No longer multiline for new fields + //multiline = data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline + + if (data.name === "body") { + //console.log("BODY: ", data) + if (hideBody === false) { + return hideBodyButtonValue + } + + rows = "4" + multiline = true + disabled = false + } + + const description = data.description === undefined ? "" : data?.description; + + const tooltipDescription = ( + + + + {tmpitem.charAt(0).toUpperCase() + tmpitem.slice(1)} + + { + setUiBox("closed") + }} + > + + + + + + + Required: {data.required === true || data.configuration === true ? "True" : "False"} + + + Description: {description} + + + Ex. : {data?.example?.length > 0 ? data.example : "No example available"} + + { + data?.configuration === true ? + ( + + Auth: Use "\$" instead of "$" + + ) : null + } + +
    { + e.preventDefault() + e.stopPropagation() + + localStorage.setItem("disabled_ui_box", "true") + setUiBox("closed") + }}> + + Don't show again + +
    +
    +
    + ); + + if (selectedApp.name === "email") { + //hideBody = false + showButtonField = false + hideBodyButtonValue = null + } + + + if ((multiline === undefined || multiline === false) && ((data?.autocompleted === true || data?.field_active === true) || data.name.startsWith("${") && data.name.endsWith("}"))) { + multiline = true + } + + if (data?.autocompleted === true || data?.field_active === true) { + rows = "1" + } + + var datafield = ( + + + + + { + event.preventDefault() + + // Get cursor position + // This makes it so we can put it in the right location? + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + + ), + }} + multiline={multiline} + onClick={() => { + /* + setExpansionModalOpen(false); + */ + + if (setScrollConfig !== undefined && scrollConfig !== null && scrollConfig !== undefined && scrollConfig.selected !== clickedFieldId) { + + scrollConfig.selected = clickedFieldId + setScrollConfig(scrollConfig) + } + }} + minRows={rows} + maxRows={6} + color="primary" + // defaultValue={data.value} + value={ + data?.value + } + error={ + data?.error?.length > 0 ? true : false + } + helperText={data?.error?.length > 0 ? errorHelperText(data?.name, data?.value, data?.error) : returnHelperText(data.name, data.value)} + //options={{ + // theme: 'gruvbox-dark', + // keyMap: 'sublime', + // mode: 'python', + //}} + //height={multiline ? 50 : 150} + type={ + placeholder.includes("***") || + (data.configuration && + (data.name.toLowerCase().includes("api") || + data.name.toLowerCase().includes("key") || + data.name.toLowerCase().includes("pass"))) + ? "password" + : "text" + } + placeholder={placeholder} + onChange={(event) => { + handleParamChange(event, count, data) + }} + onFocus={(event) => { + // Get local storage key "disabled_ui_box" and check if it's true + const disabledUiBox = localStorage.getItem("disabled_ui_box") + if (disabledUiBox === "true") { + } else { + //setUiBox(event.target.id) + } + }} + onBlur={(event) => { + handleParamChange(event, count, data) + + baseHelperText = calculateHelpertext(event.target.value) + if (setLastSaved !== undefined) { + setLastSaved(false) + } + + // Check if we clicked the tooltip or not + const tooltipid = "rightside_field_tooltip" + count + const foundElement = document.getElementById(tooltipid) + if (foundElement !== null && foundElement !== undefined) { + console.log("FOUND: ", foundElement) + } else { + //console.log("TOOLTIP -> NOT FOUND") + //setUiBox("closed") + } + }} + /> + + ) + + // Finds headers from a string to be used for autocompletion + const findHeaders = (inputdata) => { + var splitdata = inputdata.split("\n") + + var foundnewline = false + var allValues = [] + for (let [key, keyval] in Object.entries(splitdata)) { + const line = splitdata[key] + if (line === "") { + foundnewline = true + continue + } + + var splitvalue = "" + if (line.includes(":")) { + splitvalue = ":" + } + + if (line.includes("=")) { + splitvalue = "=" + } + + if (splitvalue.length === 0) { + allValues.push({ + key: line, + value: "", + }) + continue + } + + var splitKeys = line.split(splitvalue) + if (splitKeys.length > 1) { + allValues.push({ + key: splitKeys[0].trim(), + value: splitKeys[1].trim(), + }) + } else { + console.log("No keys for ", line) + } + } + + // Just add one + if (foundnewline) { + allValues.push({ + key: "", + value: "", + }) + } + + return allValues + } + + if (data.name.toLowerCase() === "headers") { + //var tmpheaders = findHeaders(data.value) + var tmpheaders = findHeaders(selectedActionParameters[count].value) + const tmpdatafield = +
    + {tmpheaders.map((inputdata, index) => { + const oldkey = inputdata.key + const oldval = inputdata.value + + return ( + +
    + { + console.log("Change from oldkey to new: ", oldkey, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + const tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key, keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key, keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldkey === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(e.target.value + line) + } else { + newarr.push(e.target.value + ": " + line) + } + } else { + newarr.push(line.replace(oldkey, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> + { + console.log("Change from oldval to new: ", oldval, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + var tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key, keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key, keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldval === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(line + e.target.value) + } else { + newarr.push(line + ": " + e.target.value) + } + } else { + newarr.push(line.replace(oldval, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> +
    +
    + ) + })} + +
    + } + + //const regexp = new RegExp("\W+\.", "g") + //let match + //while ((match = regexp.exec(data.value)) !== null) { + // console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`); + //} + + //const str = = data.value.search(submatch) + //console.log("FOUND? ", n) + //for (var key in keywords) { + // const keyword = keywords[key] + // if (data.value.includes(keyword)) { + // console.log("INCLUDED: ", keyword) + // } + //} + + if (files !== undefined && files !== null && data.name.toLowerCase() === "file_category") { + //selectedActionParameters[count].options.length > 0 + console.log("FileS: ", files) + if (files.namespaces !== undefined && files.namespaces !== null && files.namespaces.length > 0) { + data.options = files.namespaces + } + } + + //const keywords = ["len", "lower", "upper", "trim", "split", "length", "number", "parse", "join"] + if ( + selectedActionParameters[count].schema !== undefined && + selectedActionParameters[count].schema !== null && + selectedActionParameters[count].schema.type === "file" + ) { + datafield = ( + + + { + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + ), + }} + helperText={returnHelperText(data.name, data.value)} + fullWidth + multiline={multiline} + minRows={3} + maxRows={6} + color="primary" + defaultValue={data.value} + type={"text"} + placeholder={"The file ID to get"} + id={"rightside_field_" + count} + onChange={(event) => { + changeActionParameter(event, count, data); + }} + onBlur={(event) => { }} + /> + ) + } else if ( + (data.options !== undefined && data.options !== null && data.options.length > 0) || + (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) { + const parsedoptions = data.options !== undefined && data.options !== null && data.options.length > 0 ? data.options : selectedActionParameters[count].options + + if (selectedActionParameters[count].value === "") { + // && selectedActionParameters[count].required) { + // Rofl, dirty workaround :) + const e = { + target: { + value: parsedoptions[0], + }, + }; + + changeActionParameter(e, count, data); + } + + var multi = false + if (selectedActionParameters[count].multiselect !== undefined && selectedActionParameters[count].multiselect !== null && selectedActionParameters[count].multiselect === true) { + multi = true + + selectedActionParameters[count].value = selectedActionParameters[count].value.split(",") + } + + datafield = ( + + ); + } else if (data.variant === "STATIC_VALUE") { + staticcolor = "#FF8544"; + } + + if (data.field_active === false) { + //console.log("Field not active: ", data?.name) + return null + } + + // Shows nested list of nodes > their JSON lists + const ActionlistWrapper = (props) => { + const handleMenuClose = () => { + setShowAutocomplete(false); + + if ( + !selectedActionParameters[count].value[ + selectedActionParameters[count].value.length - 1 + ] === "$" + ) { + setShowDropdown(false); + } + + setUpdate(Math.random()); + setMenuPosition(null); + }; + + const handleItemClick = (values) => { + if (values === undefined || values === null || values.length === 0) { + return; + } + + var toComplete = selectedActionParameters[count].value.trim() + .endsWith("$") + ? values[0].autocomplete + : "$" + values[0].autocomplete; + + toComplete = toComplete.toLowerCase().replaceAll(" ", "_"); + for (let [key, keyval] in Object.entries(values)) { + if (key == 0 || values[key].autocomplete.length === 0) { + continue; + } + + toComplete += values[key].autocomplete; + } + + + + // Handles the fields under OpenAPI body to be parsed. + if (data.name.startsWith("${") && data.name.endsWith("}")) { + const paramcheck = selectedAction.parameters.find( + (param) => param.name === "body" + ) + + if (paramcheck !== undefined) { + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [ + { + key: data.name, + value: toComplete, + }, + ] + } else { + const subparamindex = paramcheck["value_replace"] + .findIndex((param) => param.key === data.name); + + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + key: data.name, + value: toComplete, + }) + + } else { + paramcheck["value_replace"][subparamindex]["value"] += + toComplete; + } + } + + selectedActionParameters[count]["value_replace"] = paramcheck; + + selectedAction.parameters = selectedActionParameters + //selectedAction.parameters[count]["value_replace"] = paramcheck; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + return; + } + } + + console.log("In nestedclick!!") + var newValue = selectedActionParameters[count].value + toComplete + changeActionParameter({ target: { value: newValue } }, count, data, true) + //selectedActionParameters[count].value += toComplete; + //selectedAction.parameters[count].value = selectedActionParameters[count].value; + //setSelectedAction(selectedAction); + //setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + }; + + const iconStyle = { + marginRight: 15, + }; + + return ( + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + color: "white", + marginTop: 2, + maxHeight: 650, + }} + > + {actionlist.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : ( + + ); + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #FF8544"; + } else { + exec_text_field.style.border = ""; + } + } + + // Also doing arguments + if ( + workflow.triggers !== undefined && + workflow.triggers !== null && + workflow.triggers.length > 0 + ) { + for (let [key, keyval] in Object.entries(workflow.triggers)) { + const item = workflow.triggers[key]; + + if (cy !== undefined) { + var node = cy.getElementById(item.id); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + } + } + }; + + const handleActionHover = (inside, actionId) => { + if (cy !== undefined) { + var node = cy.getElementById(actionId); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + }; + + const handleMouseover = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + //menuPosition.left -= 50 + //menuPosition.top -= 250 + //console.log("POS: ", menuPosition1) + var menuPosition1 = menuPosition + if (menuPosition1 === null) { + menuPosition1 = { + "left": 0, + "top": 0, + } + } else if (menuPosition1.top === null || menuPosition1.top === undefined) { + menuPosition1.top = 0 + } else if (menuPosition1.left === null || menuPosition1.left === undefined) { + menuPosition1.left = 0 + } + + //console.log("POS1: ", menuPosition1) + + return parsedPaths.length > 0 ? ( + + {icon} {innerdata.name} +
    + } + parentMenuOpen={!!menuPosition} + style={{ + color: "white", + minWidth: 250, + maxWidth: 250, + maxHeight: 50, + overflow: "hidden", + }} + onClick={() => { + console.log(innerdata.example) + handleItemClick([innerdata]); + }} + > + + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + + {innerdata.name} + + + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + // + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ); + // + + const indentation_count = (pathdata.name.match(/\./g) || []).length + 1 + const baseIndent =
    + //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 + const boxPadding = 0 + const namesplit = pathdata.name.split(".") + const newname = namesplit[namesplit.length - 1] + return ( + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
    + {Array(indentation_count).fill().map((subdata, subindex) => { + return ( + baseIndent + ) + })} + {icon} {newname} + {pathdata.type === "list" ? { + e.preventDefault() + e.stopPropagation() + + console.log("INNER: ", innerdata, pathdata) + + // Removing .list from autocomplete + var newname = pathdata.name + if (newname.length > 5) { + newname = newname.slice(0, newname.length - 5) + } + + //selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` + selectedActionParameters[count].value += `$${innerdata.name}.${newname}` + selectedAction.parameters[count].value = selectedActionParameters[count].value; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + setShowDropdown(false); + setMenuPosition(null); + }} /> : null} +
    +
    +
    + ); + })} + + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + +
    + {icon} {innerdata.name} +
    +
    +
    + ); + })} + + ); + } + + const buttonTitle = `Authenticate API ${selectedApp.name.replaceAll("_", " ")}` + const hasAutocomplete = data?.autocompleted === true + if (data.variant === undefined || data.variant === null) { + data.variant = "STATIC_VALUE" + } + + var isFirstOptional = optionalFound === false && data.configuration === false && data.required === false ? true : false + if (optionalFound === false && data.configuration === false && data.required === false) { + optionalFound = true + } + + if (isFirstOptional) { + // Check if any required fields are found + var foundRequired = false + for (var key in selectedActionParameters) { + if (selectedActionParameters[key]?.required === true) { + foundRequired = true break } } - } - return ( - - ); - }} - /> -
    - : null} - {selectedAction.description !== undefined && selectedAction.description !== null && selectedAction.description.length > 0 && hiddenParameters === false ? ( -
    - - Description - - - {selectedAction.description} - -
    - ) : null} - - {suggestionInfo()} - {selectedActionParameters?.map((data, count) => { - if (data.variant === "") { - data.variant = "STATIC_VALUE"; - } - - if (isIntegration && data.name === "app_name") { - return null - } - - if (data.value === "authgroup controlled") { - if (data?.name === "url" && authenticationType?.type === "oauth2-app") { - } else { - return null - } - } - - if (selectedAction.parameters === undefined || selectedAction.parameters === null || selectedAction.parameters.length !== selectedActionParameters.length) { - - //selectedAction.parameters = selectedActionParameters - //console.log("PARAM BUG - length change(?): ", selectedAction) - } - - //!selectedAction.auth_not_required && - if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { - - // This sets the placeholder in the frontend. (Replaced in backend) - if (selectedActionParameters[count] !== undefined) { - selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name] - } - - if (selectedAction.parameters[count] !== undefined) { - selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name] - } - - setSelectedAction(selectedAction); - //setUpdate(Math.random()) - - if (authWritten) { - return null; - } - - authWritten = true; - return ( - - Authentication fields are hidden - - ); - } - - - // Added autofill to make this ALOT simpler - if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { - if (selectedActionParameters[count].length === 0) { - selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" - setSelectedAction(selectedAction) - } - - return null - } - - var staticcolor = "inherit"; - var actioncolor = "inherit"; - var varcolor = "inherit"; - var multiline - if ( - data.multiline !== undefined && - data.multiline !== null && - data.multiline === true - ) { - multiline = true; - } - - // make data.value from array to comma separated string if it is an array - if (data.value !== undefined && data.value !== null && Array.isArray(data.value)) { - data.value = data.value.join(",") - } - - if (data.value !== undefined && data.value !== null && - data.value.startsWith("{") && data.value.endsWith("}")) { - multiline = true - } - - var placeholder = "Value"; - if (data.example !== undefined && data.example !== null && data.example.length > 0) { - - placeholder = data.example; - - // if (data.name === "url") { - // data.value = data.example; - // } - // In case of data.example - if (data.value === undefined || data.value === null) { - data.value = "" - } - - if (data.value.length === 0) { - if (data.name.toLowerCase() === "headers") { - //console.log("Should show headers field instead with + and -!") - - // Check if file ID exists - // - const fileFound = selectedActionParameters.find(param => param.name === "file_id") - if (fileFound === undefined || fileFound === null) { - data.value = data.example - } else { - // Purposely unset it if set by default when using files - data.value = "" - } - } - } - - /* - if (data.name !== "queries" && data.name !== "key" && data.name !== "value" ) { - data.value = data.example - } - } - */ - } - - if (selectedAction.name === "custom_action" && data.name === "body") { - for (var key in selectedActionParameters) { - const param = selectedActionParameters[key] - if (param.name === "method") { - if (param.value === "GET") { - return null - } - } - } - } - - if (data.name.startsWith("${") && data.name.endsWith("}")) { - const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); - - - if (paramcheck !== undefined && paramcheck !== null) { - if ( - paramcheck["value_replace"] !== undefined && - paramcheck["value_replace"] !== null - ) { - //console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"]) - const subparamindex = paramcheck["value_replace"].findIndex( - (param) => param.key === data.name - ); - if (subparamindex !== -1) { - data.value = - paramcheck["value_replace"][subparamindex]["value"]; - } - } - } - } - - - var showCacheConfig = false - if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") { - // Show a key popout button - showCacheConfig = true - } - - var disabled = false; - var rows = "3"; - var openApiHelperText = "This is an OpenAPI specific field"; - - - if (selectedApp.generated && data.name === "headers") { - //console.log("HEADER: ", data) - //if (data.value.length === 0) { - //} - //setSelectedActionParameters(selectedActionParameters) - } - - var hideBodyButtonValue = ( -
    - - { - // Set localstorage - localStorage.setItem("hideBody", "true") - - setHideBody(false) - const updatedParameters = selectedActionParameters.map((param) => { - if (param.name === "body") { - return { - ...param, - id: "UNTOGGLED", - } - } - - if (param.description === openApiFieldDesc) { - // Check required fields here - if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { - // Look for the field name in the required_body_fields - if (selectedAction.required_body_fields.includes(param.name)) { - param.required = true - } else { - param.required = false - } - } - - return { ...param, field_active: true } - } - - return param - }) - - setSelectedActionParameters(updatedParameters) - }} - /> - { - localStorage.setItem("hideBody", "false") - setHideBody(true) - // Make sure the body field is shown - const updatedParameters = selectedActionParameters.map((param) => { - if (param.name === "body") { - return { - ...param, - id: "TOGGLED", - } - } - - if (param.description === openApiFieldDesc) { - return { ...param, field_active: false } - } - - return param - }) - - setSelectedActionParameters(updatedParameters) - }} - /> - - {/* - - - - - - - - - */} -
    - ) + {hasAutocomplete === true ? + data.field_active === true ? + + + - var showButtonField = false - if (selectedApp.generated === true && data.name === "body") { - const regex = /\${(\w+)}/g; - const found = placeholder.match(regex); + : + + + + : + null} - var newhidebody = hideBody - showButtonField = true - if (found === undefined || found === null || found.length === 0) { - newhidebody = false - hideBodyButtonValue = null + {showCacheConfig === true ? + + + + + + : null} - if (hideBody === false) { - setHideBody(true) - } - } - - if (newhidebody === true) { - //toast("BODYBUTTON TRUE") - } else { - - rows = "1"; - disabled = true; - openApiHelperText = "OpenAPI spec: fill the following fields."; - - var changed = false; - var tempArray = [] - for (let specKey in found) { - const tmpitem = found[specKey]; - var skip = false; - - for (let innerkey in selectedActionParameters) { - if (selectedActionParameters[innerkey].name === tmpitem) { - skip = true; - break; - } - } - - if (skip) { - //console.log("SKIPPING ", tmpitem) - continue; - } - - changed = true; - var isRequired = false - // Check if original field name is in the selectedAction.required_body_fields - if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null) { - for (let innerkey in selectedAction.required_body_fields) { - if (selectedAction.required_body_fields[innerkey] === tmpitem) { - isRequired = true - break - } - } - } - - tempArray.push({ - action_field: "", - configuration: false, - description: openApiFieldDesc, - example: "", - id: "", - multiline: false, - name: tmpitem, - options: null, - required: isRequired, - schema: { type: "string" }, - skip_multicheck: false, - tags: null, - value: "", - variant: "STATIC_VALUE", - field_active: true, - - autocompleted: false, - }); - } - - var required = selectedActionParameters.filter(item => item.required === true) - var notRequired = selectedActionParameters.filter(item => item.required === false) - - if (tempArray.length > 0) { - // Sort tempArray based on tempArray.required - tempArray.sort((a, b) => (a.required < b.required) ? 1 : -1) - // Add all items to the selectedActionParameters array - for (let innerkey in tempArray) { - tempArray[innerkey].id = "ADDED" - - if (tempArray[innerkey].required === true) { - required.push(tempArray[innerkey]) - } else { - notRequired.push(tempArray[innerkey]) - } - } - } - - if (changed) { - // Sort selectedActionParameters based on selectedActionParameters.required - // Find the "headers" and "queries" field names and put them on the first indexes anyway - var newArray = required.concat(notRequired) - - - setSelectedActionParameters(newArray) - } - - } - } - - const clickedFieldId = "rightside_field_" + count; - - var baseHelperText = "" - if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) { - baseHelperText = calculateHelpertext(data.value) - } - - var tmpitem = data.name.valueOf(); - if (data.name.startsWith("${") && data.name.endsWith("}")) { - tmpitem = tmpitem.slice(2, data.name.length - 1); - } - - if (tmpitem === "from_shuffle") { - tmpitem = "from" - } - - tmpitem = ( - tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1) - ).replaceAll("_", " "); - - if (tmpitem === "Username basic") { - tmpitem = "Username" - } else if (tmpitem === "Password basic") { - tmpitem = "Password" - } - - // No longer multiline for new fields - //multiline = data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline - - if (data.name === "body") { - //console.log("BODY: ", data) - if (hideBody === false) { - return hideBodyButtonValue - } - - rows = "4" - multiline = true - disabled = false - } - - const description = data.description === undefined ? "" : data?.description; - - const tooltipDescription = ( - - - - {tmpitem.charAt(0).toUpperCase() + tmpitem.slice(1)} - - { - setUiBox("closed") - }} - > - - - - - - - Required: {data.required === true || data.configuration === true ? "True" : "False"} - - - Description: {description} - - - Ex. : {data?.example?.length > 0 ? data.example : "No example available"} - - { - data?.configuration === true ? - ( - - Auth: Use "\$" instead of "$" - - ) : null - } - -
    { - e.preventDefault() - e.stopPropagation() - - localStorage.setItem("disabled_ui_box", "true") - setUiBox("closed") - }}> - - Don't show again - -
    -
    -
    - ); - - - var datafield = ( - - - - - { - event.preventDefault() - - // Get cursor position - // This makes it so we can put it in the right location? - setMenuPosition({ - top: event.pageY + 10, - left: event.pageX + 10, - }); - setShowDropdownNumber(count); - setShowDropdown(true); - setShowAutocomplete(true); - }} - /> - - - - ), - }} - multiline={multiline} - onClick={() => { - /* - setExpansionModalOpen(false); - */ - - if (setScrollConfig !== undefined && scrollConfig !== null && scrollConfig !== undefined && scrollConfig.selected !== clickedFieldId) { - - scrollConfig.selected = clickedFieldId - setScrollConfig(scrollConfig) - } - }} - rows={data.name.startsWith("${") && data.name.endsWith("}") ? 2 : rows} - color="primary" - // defaultValue={data.value} - value={ - data?.value - } - error={ - data?.error?.length > 0 ? true : false - } - helperText={data?.error?.length > 0 ? errorHelperText(data?.name,data?.value,data?.error) : returnHelperText(data.name, data.value)} - //options={{ - // theme: 'gruvbox-dark', - // keyMap: 'sublime', - // mode: 'python', - //}} - //height={multiline ? 50 : 150} - type={ - placeholder.includes("***") || - (data.configuration && - (data.name.toLowerCase().includes("api") || - data.name.toLowerCase().includes("key") || - data.name.toLowerCase().includes("pass"))) - ? "password" - : "text" - } - placeholder={placeholder} - onChange={(event) => { - //changeActionParameterCodemirror(event, count, data) - // changeActionParameter(event, count, data); - handleParamChange(event, count, data) - }} - onFocus={(event) => { - // Get local storage key "disabled_ui_box" and check if it's true - const disabledUiBox = localStorage.getItem("disabled_ui_box") - if (disabledUiBox === "true") { - } else { - //setUiBox(event.target.id) - } - }} - onBlur={(event) => { - baseHelperText = calculateHelpertext(event.target.value) - if (setLastSaved !== undefined) { - setLastSaved(false) - } - - // Check if we clicked the tooltip or not - const tooltipid = "rightside_field_tooltip" + count - const foundElement = document.getElementById(tooltipid) - if (foundElement !== null && foundElement !== undefined) { - console.log("FOUND: ", foundElement) - } else { - //console.log("TOOLTIP -> NOT FOUND") - //setUiBox("closed") - } - }} - /> - - ); - - // Finds headers from a string to be used for autocompletion - const findHeaders = (inputdata) => { - var splitdata = inputdata.split("\n") - - var foundnewline = false - var allValues = [] - for (let [key,keyval] in Object.entries(splitdata)) { - const line = splitdata[key] - if (line === "") { - foundnewline = true - continue - } - - var splitvalue = "" - if (line.includes(":")) { - splitvalue = ":" - } - - if (line.includes("=")) { - splitvalue = "=" - } - - if (splitvalue.length === 0){ - allValues.push({ - key: line, - value: "", - }) - continue - } - - var splitKeys = line.split(splitvalue) - if (splitKeys.length > 1) { - allValues.push({ - key: splitKeys[0].trim(), - value: splitKeys[1].trim(), - }) - } else { - console.log("No keys for ", line) - } - } - - // Just add one - if (foundnewline) { - allValues.push({ - key: "", - value: "", - }) - } - - return allValues - } - - if (data.name.toLowerCase() === "headers") { - //var tmpheaders = findHeaders(data.value) - var tmpheaders = findHeaders(selectedActionParameters[count].value) - const tmpdatafield = -
    - {tmpheaders.map((inputdata, index) => { - const oldkey = inputdata.key - const oldval = inputdata.value - - return ( - -
    - { - console.log("Change from oldkey to new: ", oldkey, e.target.value) - - // Find the right line to replace! - //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) - const tmpsplit = selectedActionParameters[count].value.split("\n") - var valsplit = [] - var add_empty = false - for (let [key,keyval] in Object.entries(tmpsplit)) { - if (tmpsplit[key] === "") { - add_empty = true - continue - } - - valsplit.push(tmpsplit[key]) - } - - if (add_empty) { - valsplit.push("") - } - console.log("Split: ", valsplit) - - var newarr = [] - for (let [key,keyval] in Object.entries(valsplit)) { - var line = valsplit[key] - - if (key == index) { - if (oldkey === "") { - if (line.includes("=") || line.includes(":")) { - newarr.push(e.target.value + line) - } else { - newarr.push(e.target.value + ": " + line) - } - } else { - newarr.push(line.replace(oldkey, e.target.value, 1)) - } - - } else { - newarr.push(line) - } - } - - var newval = newarr.join("\n") - console.log("Fixed: ", newval) - - selectedActionParameters[count].value = newval - selectedAction.parameters[count].value = newval - setSelectedAction(selectedAction) - setSelectedActionParameters(selectedActionParameters) - }} - /> - { - console.log("Change from oldval to new: ", oldval, e.target.value) - - // Find the right line to replace! - //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) - var tmpsplit = selectedActionParameters[count].value.split("\n") - var valsplit = [] - var add_empty = false - for (let [key,keyval] in Object.entries(tmpsplit)) { - if (tmpsplit[key] === "") { - add_empty = true - continue - } - - valsplit.push(tmpsplit[key]) - } - - if (add_empty) { - valsplit.push("") - } - console.log("Split: ", valsplit) - - var newarr = [] - for (let [key,keyval] in Object.entries(valsplit)) { - var line = valsplit[key] - - if (key == index) { - if (oldval === "") { - if (line.includes("=") || line.includes(":")) { - newarr.push(line + e.target.value) - } else { - newarr.push(line + ": " + e.target.value) - } - } else { - newarr.push(line.replace(oldval, e.target.value, 1)) - } - - } else { - newarr.push(line) - } - } - - var newval = newarr.join("\n") - console.log("Fixed: ", newval) - - selectedActionParameters[count].value = newval - selectedAction.parameters[count].value = newval - setSelectedAction(selectedAction) - setSelectedActionParameters(selectedActionParameters) - }} - /> +
    + {tmpitem} {selectedActionParameters[count].required || selectedActionParameters[count].configuration ? "*" : ""}
    - - ) - })} - -
    - } + {parentParamValue !== undefined && parentParamValue !== null && parentParamValue !== "" && parentParamValue !== data.value ? + + { + selectedActionParameters[count].value = parentParamValue + selectedAction.parameters[count].value = parentParamValue + setSelectedAction(selectedAction) + setUpdate(Math.random()) + }} + /> + + : null} - //const regexp = new RegExp("\W+\.", "g") - //let match - //while ((match = regexp.exec(data.value)) !== null) { - // console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`); - //} + - //const str = = data.value.search(submatch) - //console.log("FOUND? ", n) - //for (var key in keywords) { - // const keyword = keywords[key] - // if (data.value.includes(keyword)) { - // console.log("INCLUDED: ", keyword) - // } - //} + { + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== null) { + clickedField.focus() + } + }} + onClick={(event) => { + // Set focus to the Textfield we just clicked + // This is to ensure focus is set correctly at all times with blur + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== null) { + clickedField.focus() + } - if (files !== undefined && files !== null && data.name.toLowerCase() === "file_category") { - //selectedActionParameters[count].options.length > 0 - console.log("FileS: ", files) - if (files.namespaces !== undefined && files.namespaces !== null && files.namespaces.length > 0) { - data.options = files.namespaces - } - } + event.preventDefault() + setFieldCount(count) + setExpansionModalOpen(true) + setActiveDialog("codeeditor") + //setcodedata(data.value) + var parsedvalue = data.value + if (parsedvalue === undefined || parsedvalue === null) { + parsedvalue = "" + } - //const keywords = ["len", "lower", "upper", "trim", "split", "length", "number", "parse", "join"] - if ( - selectedActionParameters[count].schema !== undefined && - selectedActionParameters[count].schema !== null && - selectedActionParameters[count].schema.type === "file" - ) { - datafield = ( - - - { - setMenuPosition({ - top: event.pageY + 10, - left: event.pageX + 10, - }); - setShowDropdownNumber(count); - setShowDropdown(true); - setShowAutocomplete(true); - }} - /> - - - ), - }} - helperText={returnHelperText(data.name, data.value)} - fullWidth - multiline={multiline} - rows={"3"} - color="primary" - defaultValue={data.value} - type={"text"} - placeholder={"The file ID to get"} - id={"rightside_field_" + count} - onChange={(event) => { - changeActionParameter(event, count, data); - }} - onBlur={(event) => {}} - /> - ) - } else if ( - (data.options !== undefined && data.options !== null && data.options.length > 0) || - (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) { - const parsedoptions = data.options !== undefined && data.options !== null && data.options.length > 0 ? data.options : selectedActionParameters[count].options + //console.log("Required fields: ", selectedActionParameters[count]) + navigate(`?action_id=${selectedAction.id}&field=${data.name}&action_name=${selectedAction.name}&app_name=${selectedAction?.label}`) + setEditorData({ + "name": data.name, + "value": fixExample(parsedvalue), + "field_number": count, + "actionlist": actionlist, + "field_id": clickedFieldId, - if (selectedActionParameters[count].value === "") { - // && selectedActionParameters[count].required) { - // Rofl, dirty workaround :) - const e = { - target: { - value: parsedoptions[0], - }, - }; + "example": fixExample(selectedActionParameters[count].example), + }) - changeActionParameter(e, count, data); - } + }} + /> + - var multi = false - if (selectedActionParameters[count].multiselect !== undefined && selectedActionParameters[count].multiselect !== null && selectedActionParameters[count].multiselect === true) { - multi = true - selectedActionParameters[count].value = selectedActionParameters[count].value.split(",") - } +
    + {datafield} + {/*shufflecode*/} + {showDropdown && + showDropdownNumber === count && + data.variant === "STATIC_VALUE" && + jsonList.length > 0 ? ( + + + Autocomplete + + { - console.log("MULTI SELECT: ", multi, e.target.value) + if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length - 1] === ".") { + setShowDropdown(false); + } - changeActionParameter(e, count, data); - setUpdate(Math.random()); - }} - style={{ - backgroundColor: theme.palette.surfaceColor, - color: "white", - height: "50px", - borderRadius: theme.palette?.borderRadius, - }} - > - {parsedoptions.map( - (data, index) => { - const split_data = data.split("||"); - var viewed_data = data - if (split_data.length > 1) { - viewed_data = split_data[0] - } + setUpdate(Math.random()); + }} + onClick={() => { + setShowAutocomplete(true) + }} + fullWidth + open={showAutocomplete} + style={{ + color: "white", + height: 35, + marginTop: 2, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(e) => { + console.log("SELECT ONCHANGE DONE") - viewed_data = (viewed_data.charAt(0).toUpperCase() + viewed_data.slice(1)).replaceAll("_", " ") + if (selectedActionParameters[count].value[selectedActionParameters[count].value.length - 1] === ".") { + e.target.value.autocomplete = e.target.value.autocomplete.slice(1, e.target.value.autocomplete.length); + } - return ( - - {viewed_data} - - ); - } - )} - - ); - } else if (data.variant === "STATIC_VALUE") { - staticcolor = "#FF8544"; - } + selectedActionParameters[count].value += e.target.value.autocomplete; + selectedAction.parameters[count].value = selectedActionParameters[count].value; + setSelectedAction(selectedAction); + setUpdate(Math.random()); - if (data.field_active === false) { - console.log("Field not active: ", data?.name) - return null - } + setShowDropdown(false); + }} + > + {jsonList.map((data) => { + const iconStyle = { + marginRight: 15, + }; - // Shows nested list of nodes > their JSON lists - const ActionlistWrapper = (props) => { - const handleMenuClose = () => { - setShowAutocomplete(false); + const icon = + data.type === "value" ? ( + + ) : data.type === "list" ? ( + + ) : ( + + ) - if ( - !selectedActionParameters[count].value[ - selectedActionParameters[count].value.length - 1 - ] === "$" - ) { - setShowDropdown(false); - } - - setUpdate(Math.random()); - setMenuPosition(null); - }; - - const handleItemClick = (values) => { - if (values === undefined ||values === null ||values.length === 0) { - return; - } - - var toComplete = selectedActionParameters[count].value.trim() - .endsWith("$") - ? values[0].autocomplete - : "$" + values[0].autocomplete; - - toComplete = toComplete.toLowerCase().replaceAll(" ", "_"); - for (let [key,keyval] in Object.entries(values)) { - if (key == 0 || values[key].autocomplete.length === 0) { - continue; - } - - toComplete += values[key].autocomplete; - } - - - - // Handles the fields under OpenAPI body to be parsed. - if (data.name.startsWith("${") && data.name.endsWith("}")) { - const paramcheck = selectedAction.parameters.find( - (param) => param.name === "body" - ) - - if (paramcheck !== undefined) { - if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { - paramcheck["value_replace"] = [ - { - key: data.name, - value: toComplete, - }, - ] - } else { - const subparamindex = paramcheck["value_replace"] - .findIndex((param) => param.key === data.name); - - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - key: data.name, - value: toComplete, - }) - - } else { - paramcheck["value_replace"][subparamindex]["value"] += - toComplete; - } - } - - selectedActionParameters[count]["value_replace"] = paramcheck; - - selectedAction.parameters = selectedActionParameters - //selectedAction.parameters[count]["value_replace"] = paramcheck; - setSelectedAction(selectedAction); - setUpdate(Math.random()); - - setShowDropdown(false); - setMenuPosition(null); - return; - } - } - - console.log("In nestedclick!!") - var newValue = selectedActionParameters[count].value + toComplete - changeActionParameter({target: {value: newValue}}, count, data, true) - //selectedActionParameters[count].value += toComplete; - //selectedAction.parameters[count].value = selectedActionParameters[count].value; - //setSelectedAction(selectedAction); - //setUpdate(Math.random()); - - setShowDropdown(false); - setMenuPosition(null); - }; - - const iconStyle = { - marginRight: 15, - }; - - return ( - { - handleMenuClose(); - }} - open={!!menuPosition} - style={{ - color: "white", - marginTop: 2, - maxHeight: 650, - }} - > - {actionlist.map((innerdata) => { - const icon = - innerdata.type === "action" ? ( - - ) : innerdata.type === "workflow_variable" || - innerdata.type === "execution_variable" ? ( - - ) : ( - - ); - - const handleExecArgumentHover = (inside) => { - var exec_text_field = document.getElementById( - "execution_argument_input_field" - ); - if (exec_text_field !== null) { - if (inside) { - exec_text_field.style.border = "2px solid #FF8544"; - } else { - exec_text_field.style.border = ""; - } - } - - // Also doing arguments - if ( - workflow.triggers !== undefined && - workflow.triggers !== null && - workflow.triggers.length > 0 - ) { - for (let [key,keyval] in Object.entries(workflow.triggers)) { - const item = workflow.triggers[key]; - - if (cy !== undefined) { - var node = cy.getElementById(item.id); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } - } - } - } - } - }; - - const handleActionHover = (inside, actionId) => { - if (cy !== undefined) { - var node = cy.getElementById(actionId); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } - } - } - }; - - const handleMouseover = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(true); - } else if (innerdata.type === "action") { - handleActionHover(true, innerdata.id); - } - }; - - const handleMouseOut = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(false); - } else if (innerdata.type === "action") { - handleActionHover(false, innerdata.id); - } - }; - - var parsedPaths = []; - if (typeof innerdata.example === "object") { - parsedPaths = GetParsedPaths(innerdata.example, ""); - } - - const coverColor = "#82ccc3" - //menuPosition.left -= 50 - //menuPosition.top -= 250 - //console.log("POS: ", menuPosition1) - var menuPosition1 = menuPosition - if (menuPosition1 === null) { - menuPosition1 = { - "left": 0, - "top": 0, - } - } else if (menuPosition1.top === null || menuPosition1.top === undefined) { - menuPosition1.top = 0 - } else if (menuPosition1.left === null || menuPosition1.left === undefined) { - menuPosition1.left = 0 + return ( + { }} + > + +
    + {icon} {data.name} +
    +
    +
    + ); + })} + + + ) : null} + {showDropdown && + showDropdownNumber === count && + data.variant === "STATIC_VALUE" && + jsonList.length === 0 ? ( + + ) : null} +
    + ); + })} +
    + : null } - //console.log("POS1: ", menuPosition1) - - return parsedPaths.length > 0 ? ( - - {icon} {innerdata.name} -
    - } - parentMenuOpen={!!menuPosition} - style={{ - color: "white", - minWidth: 250, - maxWidth: 250, - maxHeight: 50, - overflow: "hidden", - }} - onClick={() => { - console.log("CLICKED: ", innerdata); - console.log(innerdata.example) - handleItemClick([innerdata]); - }} - > - - { - //console.log("HOVER: ", pathdata); - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - - {innerdata.name} - - - {parsedPaths.map((pathdata, index) => { - // FIXME: Should be recursive in here - // - const icon = - pathdata.type === "value" ? ( - - ) : pathdata.type === "list" ? ( - - ) : ( - - ); - // - - const indentation_count = (pathdata.name.match(/\./g) || []).length+1 - const baseIndent =
    - //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 - const boxPadding = 0 - const namesplit = pathdata.name.split(".") - const newname = namesplit[namesplit.length-1] - return ( - { - //console.log("HOVER: ", pathdata); - }} - onClick={() => { - handleItemClick([innerdata, pathdata]); - }} - > - -
    - {Array(indentation_count).fill().map((subdata, subindex) => { - return ( - baseIndent - ) - })} - {icon} {newname} - {pathdata.type === "list" ? { - e.preventDefault() - e.stopPropagation() - - console.log("INNER: ", innerdata, pathdata) - - // Removing .list from autocomplete - var newname = pathdata.name - if (newname.length > 5) { - newname = newname.slice(0, newname.length-5) - } - - //selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` - selectedActionParameters[count].value += `$${innerdata.name}.${newname}` - selectedAction.parameters[count].value = selectedActionParameters[count].value; - setSelectedAction(selectedAction); - setUpdate(Math.random()); - setShowDropdown(false); - setMenuPosition(null); - }} /> : null} -
    -
    -
    - ); - })} - - - ) : ( - handleMouseover()} - onMouseOut={() => { - handleMouseOut(); - }} - onClick={() => { - handleItemClick([innerdata]); - }} - > - -
    - {icon} {innerdata.name} -
    -
    -
    - ); - })} - - ); - } - - const buttonTitle = `Authenticate API ${selectedApp.name.replaceAll("_", " ")}` - const hasAutocomplete = data?.autocompleted === true - if (data.variant === undefined || data.variant === null) { - data.variant = "STATIC_VALUE" - } - - return ( -
    - {showButtonField === true ? hideBodyButtonValue : null} -
    - {data.configuration === true ? ( - - { - setAuthenticationModalOpen(true); - }} - /> - - ) : null} - - {hasAutocomplete === true ? - - - - : - null} - - {showCacheConfig === true ? - - - - - - : null} - -
    - {tmpitem} {selectedActionParameters[count].required || selectedActionParameters[count].configuration ? "*" : ""} -
    - - - - { - event.preventDefault() - setFieldCount(count) - setExpansionModalOpen(true) - setActiveDialog("codeeditor") - //setcodedata(data.value) - var parsedvalue = data.value - if (parsedvalue === undefined || parsedvalue === null) { - parsedvalue = "" - } - - setEditorData({ - "name": data.name, - "value": parsedvalue, - "field_number": count, - "actionlist": actionlist, - "field_id": clickedFieldId, - - "example": selectedActionParameters[count].example, - }) - }} - /> - - -
    - {datafield} - {/*shufflecode*/} - {showDropdown && - showDropdownNumber === count && - data.variant === "STATIC_VALUE" && - jsonList.length > 0 ? ( - - - Autocomplete - - - - ) : null} - {showDropdown && - showDropdownNumber === count && - data.variant === "STATIC_VALUE" && - jsonList.length === 0 ? ( - - ) : null} -
    - ); - })} -
    - : null - } - -
    -
    -
    - ); +
    +
    +
    + ); }; export default ParsedAction; diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx index ff359168..5102b72b 100644 --- a/frontend/src/components/Priorities.jsx +++ b/frontend/src/components/Priorities.jsx @@ -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 (
    - Notifications ({ + + Notification Workflow + + + The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. You can point child org notifications into the parent org notification by choosing it in the list. + + +
    + + {workflows !== undefined && workflows !== null && workflows.length > 0 ? + { + setOpenNotification(true); + }} + onClose={() => { + setOpenNotification(false); + }} + freeSolo + //autoSelect + value={workflows?.find(w => w.id === notificationWorkflow) || null} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { + backgroundColor: "#212121", + color: "white", + }, + }} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: "#212121", + borderRadius: theme.palette?.borderRadius, + height: 35, + marginBottom: 40, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + props.onMouseDown?.(null); + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { + setNotificationWorkflow(e.target.value); + }} + /> + } + {/*
    + {orgSaveButton} +
    */} +
    + + {notificationWorkflow === undefined || notificationWorkflow === null || notificationWorkflow.length === 0 ? null : +
    + + { + if (notificationWorkflow === "parent") { + toast.error("Can't open parent org's notification workflow from here.") + return + } + + window.open(`/workflows/${notificationWorkflow}?view=executions`, "_blank") + }} + > + + +
    + } + + Notifications ({ notifications?.filter((notification) => showRead === true || notification.read === false).length }) @@ -261,10 +636,12 @@ const Priorities = memo((props) => { ) : null}
    + {clickedFromOrgTab? null : } -

    Suggestions

    + +

    Suggestions

    Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
    These range from simple configurations in Shuffle to Usecases you may have missed.  { 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 ( { //setStatus(params.row.status) }}> + {userdata?.active_org?.creator_org?.length === 0 && suborgWorkflowRuns ? ( + {source} + ) : null} {foundSource} @@ -372,7 +397,7 @@ const RuntimeDebugger = (props) => { headerName: 'Workflow Name', width: 250, renderCell: (params) => ( - { +
    { setWorkflowId(params.row.workflow.id) for (let key in workflows) { @@ -382,8 +407,18 @@ const RuntimeDebugger = (props) => { } } }}> - {params.row.workflow.name} - + {params.row.workflow.name} + {params?.row?.org?.id?.length > 0 && + + {params.row.org.name} + {params.row.org.name} +
    + )} placement="top" arrow> + + + } +
    ), }, @@ -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 (
    -
    +
    -

    Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}

    -
    - - - - ), - endAdornment: ( - - {searchQuery.length > 0 && ( - setSearchQuery('')} - /> - )} - - - ), - - }} - onChange={(e)=>{handleQueryChange(e)}} - color="primary" - placeholder="Filter by Workflow Name, Status, Execution Argument, Results.." - id="shuffle_search_field" - /> -
    - -
    - {selectedWorkflowExecutions.length > 0 ? +
    +

    Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}

    + {selectedWorkflowExecutions.length > 0 ? : null} +
    +
    +
    + + + + ), + endAdornment: ( + + {searchQuery.length > 0 && ( + setSearchQuery('')} + /> + )} + + ), + + }} + onChange={(e)=>{handleQueryChange(e)}} + color="primary" + placeholder="Filter by Workflow Name, Status, Execution Argument, Results" + id="shuffle_search_field" + /> +
    + {userdata?.active_org?.creator_org?.length === 0 ? ( +
    + { + 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" + /> + Show workflow runs from suborgs +
    + ) : null} +
    +
    { - 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", }}> Status } + renderValue={(selected) => { + return selected.join(", "); + }} + MenuProps={MenuProps} + > + {selectedOrganization.child_orgs.map((org, index) => ( + + -1} /> + + + ))} + + + ) : null; + + 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); + setShowLoader(false) + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const deleteUser = (data) => { + // Just use this one? + const userId = data.id; + + const url = globalUrl + "/api/v1/users/" + userId; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 200) { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + toast("Failed to deactivate user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast( + "Failed to deactivate user. Please contact support@shuffler.io if this persists.", + ); + } else { + toast("Changed activation for user " + data.id); + } + }) + + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const handleDeleteAccount = (userID) => { + if (userID === undefined || userID === null || userID === "") { + return; + } + + const url = `${globalUrl}/api/v1/users/${userID}/remove`; + fetch(url, { + mode: "cors", + method: "DELETE", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + toast.success( + "Deleted their account. Would reload users in a few seconds.", + ); + + setTimeout(() => { + getUsers(); + }); + } else { + toast.error(`${data.reason}`); + } + }) + .catch((error) => { + console.error( + "There was a problem with deleting the account. Please try again:", + error, + ); + toast.error( + "There was a problem with the delete request. Please try again", + ); + }); + }; + + const handleVerify2FA = (userId, code) => { + const data = { + code: code, + user_id: userId, + }; + + fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, { + 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) { + } else { + //toast("Wrong code sent.") + //toast("Wrong code sent. Please try again.") + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + toast("Successfully enabled 2fa"); + + setTimeout(() => { + getUsers(); + + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); + setSelectedUserModalOpen(false); + }, 1000); + } else { + toast("Wrong code sent. Please try again."); + //toast("Failed setting 2fa: ", responseJson.reason) + } + }) + .catch((error) => { + toast("Wrong code sent. Please try again."); + //toast("Err: " + error.toString()) + }); + }; + + const get2faCode = (userId) => { + fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + if (responseJson.success === true) { + //toast(responseJson.reason) + setImage2FA(responseJson.reason); + setSecret2FA(responseJson.extra); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const generateApikey = (user) => { + const userId = user.id; + const data = { user_id: userId }; + + toast("Generating new API key"); + + var fetchdata = { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }; + + if (userId === userdata.id) { + fetchdata.method = "GET"; + } else { + fetchdata.body = JSON.stringify(data); + } + + fetch(globalUrl + "/api/v1/generateapikey", fetchdata) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } else { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if (!responseJson.success && responseJson.reason !== undefined) { + toast("Failed getting new: " + responseJson.reason); + } else { + toast("Got new API key"); + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const UpdateMFAInUserOrg = (org_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: 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, + }, + [], + { + mfa_required: !MFARequired + } + ); + setMFARequired((prev)=> !prev) + } + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: '440px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} + > + + + Add user + + + + + We will send an email to invite them to your organization. + +
    + Username + { + if(e.key === "Enter"){ + if (isCloud) { + inviteUser(modalUser); + } else { + submitUser(modalUser); + } + } + }} + onChange={(event) => + changeModalData("Username", event.target.value) + } + /> + {isCloud ? null : ( + + Password + { + if(e.key === "enter"){ + if (isCloud) { + inviteUser(modalUser); + } else { + submitUser(modalUser); + } + } + }} + onChange={(event) => + changeModalData("Password", event.target.value) + } + /> + + )} +
    + {loginInfo} +
    + + + + +
    + ); + + const run2FASetup = (data) => { + if (!show2faSetup) { + get2faCode(data.id); + } else { + // Should remove? + setImage2FA(""); + setSecret2FA(""); + } + + setShow2faSetup(!show2faSetup); + //setShow2faSetup(true); + }; + + const editUserModal = ( + { + setSelectedUserModalOpen(false); + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(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: "800px", + minHeight: "320px", + overflow: "hidden", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} + > + + + Editing {selectedUser.username} + + + + {isCloud ? null : ( +
    + { + setNewUsername(e.target.value); + }} + /> + +
    + )} + + {isCloud ? null : ( +
    + setNewPassword(e.target.value)} + /> + +
    + )} + + {userOrgEdit} + +
    + + + + + {isCloud && userdata.support && selectedUser.id !== userdata.id ? ( + + ) : null} + + {showDeleteAccountTextbox ? ( + { + setDeleteAccountText(e.target.value); + }} + /> + ) : null} +
    + {show2faSetup ? ( +
    + {/**/} + + {secret2FA !== undefined && + secret2FA !== null && + secret2FA?.length > 0 ? ( + + + Scan the image below with the two-factor authentication app on + your phone. If you can’t use a QR code, use the code{" "} + {secret2FA} instead. + + + ) : null} + {image2FA !== undefined && + image2FA !== null && + image2FA?.length > 0 ? ( + {"2 + ) : ( + + )} + + + After scanning the QR code image, the app will display a code that + you can enter below. + +
    + { + if (event.target.value.length > 6) { + return; + } + + setValue2FA(event.target.value); + }} + /> + +
    +
    + ) : null} +
    +
    + ); + + const getLogs = (ip, userId) => { + setLogsLoading(true); + console.log("logs loading: ", logsLoading); + fetch(`${globalUrl}/api/v1/users/${userId}/audit?user_ip=${ip}`, { + mode: "cors", + method: "GET", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + return response.json(); + }) + .then((responseJson) => { + console.log("ResponseJSON: ", responseJson); + if (responseJson.success === true) { + setLogs(responseJson.logs); + } else { + if ( + responseJson.success === false || + responseJson.reason !== undefined + ) { + console.log("Reason given: ", responseJson.reason); + toast("Failed getting logs: " + responseJson.reason); + setLogs([]); + } else { + toast("Failed getting logs"); + } + } + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }) + .catch((error) => { + console.log("Error: ", error); + toast("Failed getting logs. Please contact: ", error); + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }); + }; + + const logview = logsViewModal ? ( + { + setLogsViewModal(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "1200px", + minHeight: "320px", + 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, + }, + }, + }} + > + + User Logs + + + {/* ask user for which IP they want to see logs for by iterating of user.login_info */} + + + User IP + + + + + + {logsLoading && ipSelected.length !== 0 ? ( +
    + + Loading logs +
    + ) : null} + + + + + + + + {logs.map((data, index) => { + //console.log("LOG: ", data) + + return ( + // redirect user to logs + // using request id or trace id + + + + + + )})} + +
    +
    + ) : null + + return ( +
    + {modalView} + {editUserModal} + {logview} +
    +
    +
    +
    + +
    + + +
    +
    + MFA Required + { + UpdateMFAInUserOrg(selectedOrganization.id); + }} + /> +
    +
    +
    + + + {["Username", /*"API Key",*/ "Role", /*"Active",*/ "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(9) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ): users === 0 ? null + : users?.map((data, index) => { + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + const timeNow = new Date().getTime(); + + // Get the highest timestamp in data.login_info + var lastLogin = "N/A"; + if (data.login_info !== undefined && data.login_info !== null) { + var loginInfo = 0; + for (var i = 0; i < data?.login_info?.length; i++) { + if (data.login_info[i].timestamp > loginInfo) { + loginInfo = data.login_info[i].timestamp; + } + } + + if (loginInfo > 0) { + lastLogin = + new Date(loginInfo * 1000).toISOString().slice(0, 10) + + " (" + + data?.login_info?.length + + ")"; + } + } + + var userData = data.username; + if (userdata.support === true) { + userData = ( + { + setLogsViewModal(true); + setUserLogViewing(data); + + if (userLogViewing.login_info !== undefined && userLogViewing.login_info !== null && userLogViewing.login_info.length > 0) { + getLogs(userLogViewing.login_info[0].ip, userLogViewing.id) + setIpSelected(userLogViewing.login_info[0].ip); + } + }} + > + {data.username} + + ); + } + + return ( + + + {userData || 'No username'} + + )} + primaryTypographyProps={{ + style: { + maxWidth: 150, + minWidth: 100, + width: 'auto', + color: "#FF8444", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + padding: "8px 8px 8px 15px", + }, + }} + style={{display:'table-cell', verticalAlign: 'middle' }} + /> + + {/* + + { + navigator.clipboard.writeText(data.apikey); + toast.success("Apikey copied to clipboard"); + }} + > + + + + ) + } + /> + */} + + { + console.log("VALUE: ", e.target.value); + setUser(data.id, "role", e.target.value); + }} + sx={{ + backgroundColor: "#1A1A1A", + color: "white", + height: "50px", + borderRadius: "4px", + marginTop: "8px", + marginBottom: "8px", + padding: "8px", + }} + MenuProps={{ + PaperProps: { + sx: { + "& .MuiList-root": { + padding: 0, + }, + }, + }, + }} + > + + Org Admin + + + Org User + + + Org Reader + + + } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + + + + {/* + + */} + + {selectedOrganization?.child_orgs !== undefined && + selectedOrganization?.child_orgs !== null && + selectedOrganization?.child_orgs?.length > 0 ? ( + + ) : null} + + { + setSelectedUserModalOpen(true); + setSelectedUser(data); + + // Find matching orgs between current org and current user's access to those orgs + if ( + userdata?.orgs !== undefined && + userdata?.orgs !== null && + userdata?.orgs?.length > 0 && + selectedOrganization?.child_orgs !== undefined && + selectedOrganization?.child_orgs !== null && + selectedOrganization?.child_orgs?.length > 0 + ) { + var active = []; + for (var key in userdata.orgs) { + const found = + selectedOrganization.child_orgs.find( + (item) => item.id === userdata.orgs[key].id + ); + if (found !== null && found !== undefined) { + if ( + data.orgs === undefined || + data.orgs === null + ) { + continue; + } + + const subfound = data.orgs.find( + (item) => item === found.id + ); + if ( + subfound !== null && + subfound !== undefined + ) { + active.push(subfound); + } + } + } + + setMatchingOrganizations(active); + } + }} + > + edit icon + + {/* */} + + + + + ); + })} + +
    +
    +
    +
    + ); +}) + +export default UserManagmentTab; diff --git a/frontend/src/components/WorkflowTemplatePopup.jsx b/frontend/src/components/WorkflowTemplatePopup.jsx index 74459d59..3f02f706 100644 --- a/frontend/src/components/WorkflowTemplatePopup.jsx +++ b/frontend/src/components/WorkflowTemplatePopup.jsx @@ -678,7 +678,7 @@ const WorkflowTemplatePopup = (props) => { } {img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ? -
    +
    diff --git a/frontend/src/components/WorkflowTemplatePopup2.jsx b/frontend/src/components/WorkflowTemplatePopup2.jsx index 2e666637..13810251 100644 --- a/frontend/src/components/WorkflowTemplatePopup2.jsx +++ b/frontend/src/components/WorkflowTemplatePopup2.jsx @@ -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) => { { - 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} > - + Configure Workflow @@ -564,7 +579,7 @@ const WorkflowTemplatePopup = (props) => { {title === undefined || title === null || title === "" ? null : - Selected Workflow: + Selected Usecase:
    { dstapp={dstapp} title={title} description={description} - visualOnly={true} + visualOnly={true} workflowBuilt={workflowBuilt} + inputWorkflow={workflow} shownColor={shownColor} /> @@ -585,7 +601,7 @@ const WorkflowTemplatePopup = (props) => { } -
    +
    {/* Fix the timeline when errors are fixed.. how? */} {
    {workflowLoading === true ? -
    - Generating the Workflow... +
    + Generating Workflows...
    :
    - {usecaseDetails === undefined ? null : - - {usecaseDetails?.description} + {usecaseDetails === undefined || usecaseDetails === null || workflow.id !== undefined ? null : + + {usecaseDetails?.description} } - - {errorMessage !== "" ? errorMessage : ""} - + {errorMessage !== "" ? + + {errorMessage !== "" ? errorMessage : ""} + + : null} {showLoginButton ? { 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}
    -
    +
    {img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ? @@ -849,7 +877,7 @@ const WorkflowTemplatePopup = (props) => { }
    -
    +
    {parsedTitle} @@ -858,13 +886,19 @@ const WorkflowTemplatePopup = (props) => {
    + {isActive === true && errorMessage === "" ? - - - + visualOnly === true ? + + + + : + + + : ""} - {!isActive && hasInterest === true ? + {!isActive && hasInterest === true && !visualOnly ? @@ -872,7 +906,7 @@ const WorkflowTemplatePopup = (props) => {
    - {showTryitOut && !isActive ? + {showTryitOut && !isActive && !visualOnly ? + + ); + + 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 ( +
    +
    +
    + + SSO Configuration + +
    + + Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. + +
    + + {SSORequired ? "Required" : "Optional"} +
    + +
    + {/* auto privisiong in sso */} +
    + + 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. + +
    + +
    + +
    + +
    + + You can test your SSO configuration by clicking the button below. + Before testing, ensure you have set Open ID Connect or SAML SSO + credentials. + + 0 || + ssoCertificate?.length > 0 || + openidAuthorization?.length > 0 || + openidClientId?.length > 0 + ) + ? "Please ensure all SSO credentials are set before testing." + : "" + } + > + + + + +
    + + + OpenID connect + + Configure and Authorize SAML / SSO or OpenID connect. {" "} + + Learn more + + + + + IdP URL for Shuffle OpenID: {`${globalUrl}/api/v1/login_openid`} + + + + + Client ID + { + 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, + }, + }} + /> + + + + + Client Secret + { + 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, + }, + }} + /> + + + + + + + Authorization URL + { + 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, + }, + }} + /> + + + + + Token URL + { + 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, + }, + }} + /> + + + + + {/**/} + {/*isCloud ? null : */} + + SAML SSO (v1.1) + + IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} + + + + + SSO Entrypoint (IdP) + { + 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, + }, + }} + /> + + + + + SSO Certificate (X509) + { + 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, + }, + }} + /> + + + + +
    + {orgSaveButton} +
    +
    +
    +
    + ) +} + +export default SSOTab \ No newline at end of file diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index d61f5809..d7a6b68e 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -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", diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx index 68934746..ae5fcbf9 100644 --- a/frontend/src/theme.jsx +++ b/frontend/src/theme.jsx @@ -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'); + } + `, + }, }, }, })); diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx index 2e68cef6..45d933b9 100644 --- a/frontend/src/views/Admin2.jsx +++ b/frontend/src/views/Admin2.jsx @@ -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 (
    - +
    ); }; diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index bfeff035..bdbaf1c2 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -14,7 +14,7 @@ import ReactJson from "react-json-view-ssr"; import { NestedMenuItem } from 'mui-nested-menu'; import Markdown from "react-markdown"; //import { useAlert -import { ToastContainer, toast } from "react-toastify" +import { ToastContainer, toast } from "react-toastify" import { isMobile } from "react-device-detect" import aa from 'search-insights' import Drift from "react-driftjs"; @@ -72,13 +72,13 @@ import { ButtonGroup, } from "@mui/material"; -import CodeIcon from '@mui/icons-material/Code'; import { + Code as CodeIcon, Folder as FolderIcon, VerifiedUser as VerifiedUserIcon, CheckCircle as CheckCircleIcon, - Insights as InsightsIcon, + Insights as InsightsIcon, LibraryBooks as LibraryBooksIcon, OpenInNew as OpenInNewIcon, Undo as UndoIcon, @@ -87,9 +87,9 @@ import { Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, - DragIndicator as DragIndicatorIcon, + DragIndicator as DragIndicatorIcon, Error as ErrorIcon, - Warning as WarningIcon, + Warning as WarningIcon, ArrowLeft as ArrowLeftIcon, ArrowRight as ArrowRightIcon, Cached as CachedIcon, @@ -104,6 +104,7 @@ import { Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, + KeyboardArrowDown as KeyboardArrowDownIcon, Save as SaveIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, @@ -114,21 +115,22 @@ import { AddComment as AddCommentIcon, Edit as EditIcon, Send as SendIcon, - Restore as RestoreIcon, + Restore as RestoreIcon, Preview as PreviewIcon, ContentCopy as ContentCopyIcon, - Circle as CircleIcon, + Circle as CircleIcon, SquareFoot as SquareFootIcon, AutoFixHigh as AutoFixHighIcon, - Polyline as PolylineIcon, - QueryStats as QueryStatsIcon, + Polyline as PolylineIcon, + QueryStats as QueryStatsIcon, AutoAwesome as AutoAwesomeIcon, Add as AddIcon, - ErrorOutline as ErrorOutlineIcon, + ErrorOutline as ErrorOutlineIcon, ArrowForward as ArrowForwardIcon, - + OpenInFull as OpenInFullIcon, + Difference as DifferenceIcon, } from "@mui/icons-material"; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; //import * as cytoscape from "cytoscape"; @@ -154,86 +156,84 @@ import ExtraApps from "../components/ExtraApps.jsx" import EditWorkflow from "../components/EditWorkflow.jsx" import { act } from "react"; import { Context } from "../context/ContextApi.jsx"; -// import AppStats from "../components/AppStats.jsx"; -const noImage = "/public/no_image.png"; cytoscape.use(edgehandles); export const triggers = [ - { - name: "Webhook", - type: "TRIGGER", - status: "uninitialized", - trigger_type: "WEBHOOK", - errors: null, - large_image: - "data:image/png;base64,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", - is_valid: true, - label: "Webhook", - environment: "onprem", - description: "Custom HTTP input trigger", - long_description: "Execute a workflow with an unauthicated POST request", - id: "", - }, - { - name: "Schedule", - type: "TRIGGER", - status: "uninitialized", - trigger_type: "SCHEDULE", - errors: null, - large_image: - "data:image/png;base64,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", - label: "Schedule", - is_valid: true, - environment: "onprem", - description: "Schedule time trigger", - long_description: "Create a schedule based on cron", - id: "", - }, - { - name: "Pipelines", - type: "TRIGGER", - status: "uninitialized", - description: "Run a pipeline trigger", - trigger_type: "PIPELINE", - errors: null, - is_valid: true, - label: "Pipeline", - environment: "onprem", - large_image: "/images/workflows/tenzir2.png", - long_description: "Controls a pipeline to run things", - id: "", - }, - { - name: "Shuffle Workflow", - type: "TRIGGER", - status: "uninitialized", - trigger_type: "SUBFLOW", - errors: null, - large_image: - "data:image/png;base64,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", - is_valid: true, - label: "Subflow", - environment: "onprem", - description: "Run a Subflow trigger", - long_description: "Execute another workflow from this workflow", - id: "", - }, - { - name: "User Input", - type: "TRIGGER", - status: "running", - large_image: "/images/workflows/UserInput2.svg", - description: "Wait for user input trigger", - trigger_type: "USERINPUT", - is_valid: true, - errors: null, - label: "User input", - environment: "cloud", - long_description: "Take user input to continue execution", - id: "", - }, - ]; + { + name: "Webhook", + type: "TRIGGER", + status: "uninitialized", + trigger_type: "WEBHOOK", + errors: null, + large_image: + "data:image/png;base64,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", + is_valid: true, + label: "Webhook", + environment: "onprem", + description: "Custom HTTP input trigger", + long_description: "Execute a workflow with an unauthicated POST request", + id: "", + }, + { + name: "Schedule", + type: "TRIGGER", + status: "uninitialized", + trigger_type: "SCHEDULE", + errors: null, + large_image: + "data:image/png;base64,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", + label: "Schedule", + is_valid: true, + environment: "onprem", + description: "Schedule time trigger", + long_description: "Create a schedule based on cron", + id: "", + }, + { + name: "Pipelines", + type: "TRIGGER", + status: "uninitialized", + description: "Run a pipeline trigger", + trigger_type: "PIPELINE", + errors: null, + is_valid: true, + label: "Pipeline", + environment: "onprem", + large_image: "/images/workflows/tenzir2.png", + long_description: "Controls a pipeline to run things", + id: "", + }, + { + name: "Shuffle Workflow", + type: "TRIGGER", + status: "uninitialized", + trigger_type: "SUBFLOW", + errors: null, + large_image: + "data:image/png;base64,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", + is_valid: true, + label: "Subflow", + environment: "onprem", + description: "Run a Subflow trigger", + long_description: "Execute another workflow from this workflow", + id: "", + }, + { + name: "User Input", + type: "TRIGGER", + status: "running", + large_image: "/images/workflows/UserInput2.svg", + description: "Wait for user input trigger", + trigger_type: "USERINPUT", + is_valid: true, + errors: null, + label: "User input", + environment: "cloud", + long_description: "Take user input to continue execution", + id: "", + }, +]; // Adds specific text to items // https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react @@ -282,45 +282,45 @@ export function sortByKey(array, key) { // used primarily for AI autocompletions export function SetJsonDotnotation(jsonInput, inputKey) { - if (jsonInput === undefined || jsonInput === null) { - return jsonInput; - } + if (jsonInput === undefined || jsonInput === null) { + return jsonInput; + } - // Check for array - if (Array.isArray(jsonInput)) { - for (var i = 0; i < jsonInput.length; i++) { - jsonInput[i] = SetJsonDotnotation(jsonInput[i], inputKey+".#"); - } + // Check for array + if (Array.isArray(jsonInput)) { + for (var i = 0; i < jsonInput.length; i++) { + jsonInput[i] = SetJsonDotnotation(jsonInput[i], inputKey + ".#"); + } - return jsonInput; - // Check for dict - } else if (typeof jsonInput === "object") { - // Loop keys and values - - for (var key in jsonInput) { - if (!jsonInput.hasOwnProperty(key)) { - continue - } + return jsonInput; + // Check for dict + } else if (typeof jsonInput === "object") { + // Loop keys and values - const value = jsonInput[key]; - // Check if array - if (Array.isArray(value)) { - for (var i = 0; i < value.length; i++) { - jsonInput[key][i] = SetJsonDotnotation(jsonInput[key][i], inputKey+"."+key+".#"); - } - } else if (typeof value === "object") { - jsonInput[key] = SetJsonDotnotation(jsonInput[key], inputKey+"."+key); - } else { - jsonInput[key] = inputKey+"."+key - } - } - } else { - //jsonInput = inputKey + for (var key in jsonInput) { + if (!jsonInput.hasOwnProperty(key)) { + continue + } - console.log("SetJsonDotnotation: jsonInput is not an object or array, but key ", jsonInput, typeof jsonInput); - } + const value = jsonInput[key]; + // Check if array + if (Array.isArray(value)) { + for (var i = 0; i < value.length; i++) { + jsonInput[key][i] = SetJsonDotnotation(jsonInput[key][i], inputKey + "." + key + ".#"); + } + } else if (typeof value === "object") { + jsonInput[key] = SetJsonDotnotation(jsonInput[key], inputKey + "." + key); + } else { + jsonInput[key] = inputKey + "." + key + } + } + } else { + //jsonInput = inputKey - return jsonInput; + console.log("SetJsonDotnotation: jsonInput is not an object or array, but key ", jsonInput, typeof jsonInput); + } + + return jsonInput; } //export const green = "#86c142"; @@ -417,8 +417,9 @@ const AngularWorkflow = (defaultprops) => { const [editWorkflowDetails, setEditWorkflowDetails] = React.useState(false); const [workflow, setWorkflow] = React.useState({}); + const [currentWorkflow, setCurrentWorkflow] = React.useState({}); // only for suborg distribution const [originalWorkflow, setOriginalWorkflow] = React.useState({}); - const [userSettings, setUserSettings] = React.useState({}); + const [originalSelectedEnvironment, setOriginalSelectedEnvironment] = React.useState({}); const [subworkflow, setSubworkflow] = React.useState({}); const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); @@ -433,8 +434,8 @@ const AngularWorkflow = (defaultprops) => { const [appGroup, setAppGroup] = React.useState([]); const [triggerGroup, setTriggerGroup] = React.useState([]); const [executionText, setExecutionText] = React.useState(""); - const [executionRequestStarted, setExecutionRequestStarted] =React.useState(false); - + const [executionRequestStarted, setExecutionRequestStarted] = React.useState(false); + const [scrollConfig, setScrollConfig] = React.useState({ top: 0, left: 0, @@ -445,13 +446,14 @@ const AngularWorkflow = (defaultprops) => { const [historyIndex, setHistoryIndex] = React.useState(history.length); const [variableInfo, setVariableInfo] = React.useState({}) const [selectedVersion, setSelectedVersion] = React.useState(null) + const [selectedTriggerValue, setSelectedTriggerValue] = React.useState("") const [appAuthentication, setAppAuthentication] = React.useState(undefined); const [variablesModalOpen, setVariablesModalOpen] = React.useState(false); const [aiQueryModalOpen, setAiQueryModalOpen] = React.useState(false) const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = React.useState(false); const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); - + const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false); const [authenticationType, setAuthenticationType] = React.useState(""); @@ -470,7 +472,7 @@ const AngularWorkflow = (defaultprops) => { const [authGroups, setAuthGroups] = React.useState([]) const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname; - + // 0 = normal, 1 = just done, 2 = normal const [savingState, setSavingState] = React.useState(0); @@ -499,6 +501,9 @@ const AngularWorkflow = (defaultprops) => { const [activeDialog, setActiveDialog] = React.useState(""); const [visited, setVisited] = React.useState([]); const [allRevisions, setAllRevisions] = useState([]) + const [menuPosition, setMenuPosition] = useState(null); + const [showDropdown, setShowDropdown] = React.useState(false); + const [subflowActionList, setSubflowActionList] = React.useState([]); const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); @@ -554,320 +559,453 @@ const AngularWorkflow = (defaultprops) => { const [allTriggers, setAllTriggers] = React.useState(undefined) const [suggestionBox, setSuggestionBox] = React.useState({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "attachedTo": "", + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", }) useEffect(() => { - if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { - saveWorkflow(workflow) - } + if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { + saveWorkflow(workflow) + } }, [editWorkflowModalOpen]) + useEffect(() => { + if (selectedTrigger !== undefined && selectedTrigger?.parameters !== undefined && selectedTrigger?.parameters !== null && selectedTrigger?.parameters?.length > 1) { + // Right now just setting for the subflow + setSelectedTriggerValue(selectedTrigger?.parameters[1]?.value) + } + }, [selectedTrigger]) + + useEffect(() => { + if(selectedEdge && Object.keys(selectedEdge).length > 0){ + setConditionsModalOpen(false) + setCodeEditorModalOpen(false) + } + }, [selectedEdge]) + + const dragRef = React.useRef(false); + + // New for generated stuff -const releaseToConnectLabel = "Release to Connect" - const integrationApps = [{ - "id": "integration", - "name": "Singul", + const releaseToConnectLabel = "Release to Connect" + const integrationApps = [ + { + "id": "shuffle_agent", + "name": "AI Agent", "type": "ACTION", - "app_version": "1.0.0", + "app_version": "1.0.0", "loop_versions": ["1.0.0"], - "authentication": { + "authentication": { "type": "", }, - "description": "Support-use only", + "description": "AI Agent", "actions": [{ - "name": "Cases", - "description": "Available actions for case management", - "label": "Cases", - "parameters": [{ - "name": "action", - "value": "list_tickets", - "options": [ - "list_tickets", - "get_ticket", - "create_ticket", - ], - "required": true, - }, - { - "name": "fields", - "value": "", - "required": false, - "multiline": true, - }, - /*{ - "name": "options", - "value": "deduplicate,enrich", - "required": false, - "multiselect": true, - "options": [ - "deduplicate", - "enrich", - ] - }*/ + "name": "Run LLM", + "description": "Run an LLM query against any tool you want", + "label": "Run LLM", + "parameters": [ + { + "name": "app_name", + "value": "Shuffle AI", + "required": true, + "description": "The name of the app to run the LLM query against", + }, + /* + { + "name": "model", + "value": "default", + "required": true, + "description": "The model to use for the LLM query", + }, + */ + { + "name": "input", + "value": "Take the data below and run the LLM query\n\n$exec", + "required": true, + "multiline": true, + "description": "The input data for the LLM query", + }, + { + "name": "action", + "value": "", + "required": true, + "description": "The action to perform automatically after the LLM query", + "options": [ + "Nothing", + "Create ticket", + "List tickets", + "Get specific ticket", + ], + "multiselect": true, + }, ] - },{ - "name": "Communication", - "description": "Available actions for communication", - "label": "Communication", - "parameters": [{ - "name": "action", - "value": "list_messages", - "options": [ - "list_messages", - "send_message", - ], - "required": true, - }, - { - "name": "fields", - "value": "", - "required": false, - "multiline": true, - }] - }, - { - "name": "IAM", - "description": "Available actions for IAM", - "label": "IAM", - "parameters": [{ - "name": "action", - "value": "get_kms_key", - "options": [ - "get_kms_key", - ], - "required": true, - }, - { - "name": "fields", - "value": "", - "required": false, - "multiline": true, - }] - }, - ] - }] + }], + large_image: theme.palette.singulBlackWhite, + }, + { + "id": "integration", + "name": "Singul", + "large_image": theme.palette.singulGreen, + "type": "ACTION", + "app_version": "1.0.0", + "loop_versions": ["1.0.0"], + "authentication": { + "type": "", + }, + "description": "Support-use only", + "actions": [{ + "name": "Cases", + "description": "Available actions for case management", + "label": "Cases", + "parameters": [{ + "name": "action", + "value": "list_tickets", + "options": [ + "list_tickets", + "get_ticket", + "create_ticket", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }, + /*{ + "name": "options", + "value": "deduplicate,enrich", + "required": false, + "multiselect": true, + "options": [ + "deduplicate", + "enrich", + ] + }*/ + ] + }, { + "name": "Communication", + "description": "Available actions for communication", + "label": "Communication", + "parameters": [{ + "name": "action", + "value": "list_messages", + "options": [ + "list_messages", + "send_message", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "IAM", + "description": "Available actions for IAM", + "label": "IAM", + "parameters": [{ + "name": "action", + "value": "get_kms_key", + "options": [ + "get_kms_key", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + ] + }] - /* - { - "name": "Email", - "label": "Email", - "parameters": [{ - "name": "action", - "value": "list_email", - "options": [ - "list_email", - "send_mail", - ], - "required": true, - }], - }] - }] - */ + /* + { + "name": "Email", + "label": "Email", + "parameters": [{ + "name": "action", + "value": "list_email", + "options": [ + "list_email", + "send_mail", + ], + "required": true, + }], + }] + }] + */ // For code editor const [codeEditorModalOpen, setCodeEditorModalOpen] = React.useState(false); const [codedata, setcodedata] = React.useState(""); const [editorData, setEditorData] = React.useState({ - "name": "", - "value": "", - "field_number": -1, - "actionlist": [], - "field_id": "", - - "example": "", + "name": "", + "value": "", + "field_number": -1, + "actionlist": [], + "field_id": "", + + "example": "", }) const [loadedApps, setLoadedApps] = React.useState([]) - const loadAppConfig = (appId, select) => { - if (appId === undefined || appId === null || appId.length === 0) { - console.log("No appId to load") - return - } + const loadAppConfig = (appId, select) => { + if (appId === undefined || appId === null || appId.length === 0) { + console.log("No appId to load") + return + } - if (appId === "integration") { - return - } + if (appId === "integration" || appId === "shuffle_agent") { + if (appId === "shuffle_agent") { + // Get the apps for OpenAI, Gemini, Mistral, DeepSeek + loadAppConfig("1275a420a6a8b8d782483ac0c22f492c") // Shuffle AI + loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // OpenAI + //loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // Gemini + //loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // DeepSeek - if (loadedApps.includes(appId)) { - return - } + //loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // Mistral + } - loadedApps.push(appId) - setLoadedApps(loadedApps) + return + } - const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false` - fetch(appUrl, { - headers: { - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - return response.json() - }) - .then((responseJson) => { + if (loadedApps.includes(appId)) { + //console.log("App already loaded: ", appId) - if (responseJson.success === true && responseJson.app !== undefined && responseJson.app !== null && responseJson.app.length > 0) { - // Base64 decode into json - const foundapp = JSON.parse(atob(responseJson.app)) - const selectedAppActions = selectedApp.actions === undefined || selectedApp.actions === null ? [] : selectedApp.actions - if (foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > selectedAppActions.length) { + // 1. Find the app and check if it has actions + // 2. If it doesn't have actions, reload once again + var should_reload = false + for (var i = 0; i < apps.length; i++) { + const curapp = apps[i] + if (curapp.id !== appId) { + continue + } - if (select) { - setSelectedApp(foundapp) - } + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0 || curapp.actions.length === 1) { + should_reload = true + break + } + } - if (apps === undefined || apps === null || apps.length === 0) { - console.log("LOAD APPS!") - } + if (!should_reload) { + return + } + } - for (var i = 0; i < apps.length; i++) { - if (apps[i].id !== foundapp.id) { - continue - } + if (!loadedApps.includes(appId)) { + loadedApps.push(appId) + setLoadedApps(loadedApps) + } - apps[i] = foundapp - setApps(apps) - setFilteredApps(apps) - - // Update the local storage - localStorage.setItem("apps", JSON.stringify(apps)) - break - } - } - - if (cy !== undefined && cy !== null) { - - // Check if any apps in the workflow has - cy.nodes().forEach((node) => { - const data = node.data() - if (data.app_id === foundapp.id) { - - if (data.name === "tmp" && data.parameters !== undefined && data.parameters !== null && data.parameters.length === 1 && data.parameters[0].name === "tmp" && foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > 0) { - const startIndex = foundapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) - const actionIndex = startIndex < 0 ? 0 : startIndex - - node.data("name", foundapp.actions[actionIndex].name) - node.data("large_image", foundapp.large_image) - node.data("parameters", foundapp.actions[actionIndex].parameters) - node.data("finished", true) - node.data("category", foundapp.categories !== null && foundapp.categories !== undefined && foundapp.categories.length > 0 ? foundapp.categories[0] : "") - - /* - name: app.actions[actionIndex].name, - label: actionLabel, - app_name: app.name, - app_version: app.app_version, - app_id: app.id, - sharing: app.sharing, - private_id: app.private_id, - description: description, - environment: parsedEnvironments, - errors: [], - finished: false, - id_: newNodeId, - _id_: newNodeId, - id: newNodeId, - is_valid: true, - type: actionType, - parameters: parameters, - isStartNode: false, - large_image: app.large_image, - run_magic_output: false, - authentication: [], - execution_variable: undefined, - example: example, - required_body_fields: app.actions[actionIndex].required_body_fields, - authentication_id: authId, - finished: false, - template: app.template === true ? true : false, - */ - - toast("REPLACING ACTIONS") - } - } - }) - //if (action.app_id === same && app.actions.length === 1 && app.actions[0].parameters.length === 1 && app.actions[0].parameters[0].name === "tmp") { - } - - - // FIXME: Add it to the existing list AND update the selected app - } - - }) - .catch((error) => { - console.log(`Failed side-loading app ${appId}: ${error}`) - }) + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", } + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false` + fetch(appUrl, { + headers: headers, + credentials: "include", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + + if (responseJson.success === true && responseJson.app !== undefined && responseJson.app !== null && responseJson.app.length > 0) { + + // Base64 decode into json + const foundapp = JSON.parse(atob(responseJson.app)) + var selectedAppActions = selectedApp.actions === undefined || selectedApp.actions === null ? [] : selectedApp.actions + if (apps !== undefined && apps !== null && apps.length > 0 && (selectedAppActions.length === 0 || selectedAppActions.length === 1)) { + for (var appkey in apps) { + const loopedApp = apps[appkey] + if (loopedApp.id !== appId) { + continue + } + + if (loopedApp.actions === undefined || loopedApp.actions === null || loopedApp.actions.length === 0) { + break + } + + if (loopedApp.actions.length > selectedAppActions.length) { + selectedAppActions = loopedApp.actions + } + + break + } + } + + if (foundapp?.actions !== undefined && foundapp?.actions !== null && foundapp?.actions?.length > selectedAppActions?.length) { + if (select) { + setSelectedApp(foundapp) + } + + if (apps === undefined || apps === null || apps.length === 0) { + console.log("No apps to update :(") + getApps() + return + } + + for (var i = 0; i < apps.length; i++) { + if (apps[i].id !== foundapp.id) { + continue + } + + apps[i].actions = foundapp.actions + setApps(apps) + setFilteredApps(apps) + + // Update the local storage + localStorage.setItem("apps", JSON.stringify(apps)) + break + } + } else { + console.log("Found app, but no actions: ", foundapp) + } + + if (cy !== undefined && cy !== null) { + + // Check if any apps in the workflow has + cy.nodes().forEach((node) => { + const data = node.data() + if (data.app_id === foundapp.id) { + + if (data.name === "tmp" && data.parameters !== undefined && data.parameters !== null && data.parameters.length === 1 && data.parameters[0].name === "tmp" && foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > 0) { + const startIndex = foundapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) + const actionIndex = startIndex < 0 ? 0 : startIndex + + node.data("name", foundapp.actions[actionIndex].name) + node.data("large_image", foundapp.large_image) + node.data("parameters", foundapp.actions[actionIndex].parameters) + node.data("finished", true) + node.data("category", foundapp.categories !== null && foundapp.categories !== undefined && foundapp.categories.length > 0 ? foundapp.categories[0] : "") + + /* + name: app.actions[actionIndex].name, + label: actionLabel, + app_name: app.name, + app_version: app.app_version, + app_id: app.id, + sharing: app.sharing, + private_id: app.private_id, + description: description, + environment: parsedEnvironments, + errors: [], + finished: false, + id_: newNodeId, + _id_: newNodeId, + id: newNodeId, + is_valid: true, + type: actionType, + parameters: parameters, + isStartNode: false, + large_image: app.large_image, + run_magic_output: false, + authentication: [], + execution_variable: undefined, + example: example, + required_body_fields: app.actions[actionIndex].required_body_fields, + authentication_id: authId, + finished: false, + template: app.template === true ? true : false, + */ + + toast("REPLACING ACTIONS") + } + } + }) + //if (action.app_id === same && app.actions.length === 1 && app.actions[0].parameters.length === 1 && app.actions[0].parameters[0].name === "tmp") { + } + + + // FIXME: Add it to the existing list AND update the selected app + } + + }) + .catch((error) => { + console.log(`Failed side-loading app ${appId}: ${error}`) + }) + } + useEffect(() => { if (workflow.actions?.length == 1) { if (workflow.actions[0].app_id == "3e320a20966d33c9b7e6790b2705f0bf") { - setWorkflowAsCode(true); + setWorkflowAsCode(true); } } }, [workflow]); // Event for making sure app is correct useEffect(() => { - if (selectedApp === undefined || selectedApp === null && selectedApp.app_name === undefined) { - return - } + if (selectedApp === undefined || selectedApp === null && selectedApp.app_name === undefined) { + return + } - if (apps === undefined || apps === null || apps.length === 0) { - return - } + if (apps === undefined || apps === null || apps.length === 0) { + return + } - // Handle the activation case, as they are NOT in the event management system yet - if (selectedApp.actions === undefined || selectedApp.actions === null || selectedApp.actions.length > 1) { - return - } else { + // Handle the activation case, as they are NOT in the event management system yet + if (selectedApp.actions === undefined || selectedApp.actions === null || selectedApp.actions.length > 1) { + return + } else { - if (selectedApp.id !== undefined && selectedApp.id !== null && selectedApp.id.length > 0) { - loadAppConfig(selectedApp.id, true) - } - } + if (selectedApp.id !== undefined && selectedApp.id !== null && selectedApp.id.length > 0) { + loadAppConfig(selectedApp.id, true) + } + } - for (let appkey in apps) { - const curapp = apps[appkey] - if (curapp.name !== selectedApp.name) { - continue - } - - if (curapp.actions !== undefined && curapp.actions !== null && curapp.actions.length > selectedApp.actions.length) { - var foundActionIndex = -1 - for (let actionkey in curapp.actions) { - const curaction = curapp.actions[actionkey] + for (let appkey in apps) { + const curapp = apps[appkey] + if (curapp.name !== selectedApp.name) { + continue + } - // First action with a label, as they are most used (typically) - if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - foundActionIndex = actionkey - break - } - } + if (curapp.actions !== undefined && curapp.actions !== null && curapp.actions.length > selectedApp.actions.length) { + var foundActionIndex = -1 + for (let actionkey in curapp.actions) { + const curaction = curapp.actions[actionkey] - if (foundActionIndex >= 0) { - var newaction = curapp.actions[foundActionIndex] + // First action with a label, as they are most used (typically) + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + foundActionIndex = actionkey + break + } + } - setNewSelectedAction({ - "target": { - "value": newaction.name - }, - }) - } + if (foundActionIndex >= 0) { + var newaction = curapp.actions[foundActionIndex] - setSelectedApp(curapp) - } + setNewSelectedAction({ + "target": { + "value": newaction.name + }, + }) + } - break - } + setSelectedApp(curapp) + } + + break + } }, [selectedApp]) @@ -894,121 +1032,336 @@ const releaseToConnectLabel = "Release to Connect" /* // Zoom testing to try autofixing for small screens if (document !== undefined && document !== null && !isMobile) { - const currentZoom = document.body.style.zoom; + const currentZoom = document.body.style.zoom; - if (bodyWidth < 1367 || bodyHeight < 769) { - console.log("LOWER ZOOM") - document.body.style.zoom = "80%" - bodyWidth = bodyWidth*0.8 - bodyHeight = bodyHeight*0.8 - } else { - console.log("RESET ZOOM") - document.body.style.zoom = "100%" - } + if (bodyWidth < 1367 || bodyHeight < 769) { + console.log("LOWER ZOOM") + document.body.style.zoom = "80%" + bodyWidth = bodyWidth*0.8 + bodyHeight = bodyHeight*0.8 + } else { + console.log("RESET ZOOM") + document.body.style.zoom = "100%" + } } console.log("Width, height: ", bodyWidth, bodyHeight) */ //console.log("Mobile: ", isMobile, bodyWidth, bodyHeight) - + const [elements, setElements] = useState([]); const [loopRunning, setLoopRunning] = useState(false) var loopRunning2 = loopRunning const stop = () => { - setLoopRunning(false) - loopRunning2 = false + setLoopRunning(false) + loopRunning2 = false } const start = () => { - setLoopRunning(true) - loopRunning2 = true + setLoopRunning(true) + loopRunning2 = true } useEffect(() => { - if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { - setOriginalWorkflow(workflow) - } + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { + setOriginalWorkflow(workflow) + } - // Special multi-workflow edgecase handler for events - if (distributedFromParent === "" && suborgWorkflows === []) { - } else { - if (cy !== undefined) { - cy.removeListener("select"); - cy.removeListener("unselect"); - cy.removeListener("add"); - cy.removeListener("remove"); - cy.removeListener("mouseover"); - cy.removeListener("mouseout"); - cy.removeListener("drag"); - cy.removeListener("free"); - cy.removeListener("cxttap"); + // Special multi-workflow edgecase handler for events + if (distributedFromParent === "" && suborgWorkflows === []) { + } else { + if (cy !== undefined && cy !== null) { + cy.removeListener("select"); + cy.removeListener("unselect"); + cy.removeListener("add"); + cy.removeListener("remove"); + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); - setTimeout(() => { - setupGraph(workflow) + setTimeout(() => { + setupGraph(workflow) - cy.on("select", "node", (e) => { - onNodeSelect(e, appAuthentication); - }); - cy.on("select", "edge", (e) => onEdgeSelect(e)); + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); - cy.on("unselect", (e) => onUnselect(e)); + cy.on("unselect", (e) => onUnselect(e)); - cy.on("add", "node", (e) => onNodeAdded(e)); - cy.on("add", "edge", (e) => onEdgeAdded(e)); - cy.on("remove", "node", (e) => onNodeRemoved(e)); - cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); - cy.on("mouseover", "edge", (e) => onEdgeHover(e)); - cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); - cy.on("mouseover", "node", (e) => onNodeHover(e)); - cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); - // Handles dragging - cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); - cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); - cy.on("cxttap", "node", (e) => onCtxTap(e)); + cy.on("cxttap", "node", (e) => onCtxTap(e)); - cy.edgehandles({ - handleNodes: (el) => { - if (el.isNode() && - el.data("buttonType") != "ACTIONSUGGESTION" && - !el.data("isButton") && - !el.data("isDescriptor") && - !el.data("isSuggestion") && - el.data("type") !== "COMMENT") { - return true - } + cy.edgehandles({ + handleNodes: (el) => { + if (el.isNode() && + el.data("buttonType") != "ACTIONSUGGESTION" && + !el.data("isButton") && + !el.data("isDescriptor") && + !el.data("isSuggestion") && + el.data("type") !== "COMMENT") { + return true + } - return false - }, - preview: true, - toggleOffOnLeave: true, - loopAllowed: function (node) { - return false; - }, - }) - }, 50) - } - } + return false + }, + preview: true, + toggleOffOnLeave: true, + loopAllowed: function (node) { + return false; + }, + }) + }, 200) + } + } }, [workflow]) useEffect(() => { - // Current variable + future state controlled - // This is so that the loop can stop itself as well - if (loopRunning && loopRunning2) { - const intervalId = setInterval(() => { - if (!loopRunning) { - clearInterval(intervalId); - } + // if (subflowActionList.length === 0) { + const newActionList = []; + // FIXME: Have previous execution values in here + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "hello", + }) + newActionList.push({ + type: "Shuffle Database", + name: "Shuffle Database", + value: "$shuffle_cache", + highlight: "shuffle_db", + autocomplete: "shuffle_cache", + example: "hello", + }) + if ( + workflow.workflow_variables !== null && + workflow.workflow_variables !== undefined && + workflow.workflow_variables.length > 0 + ) { + for (let varkey in workflow.workflow_variables) { + const item = workflow.workflow_variables[varkey]; + newActionList.push({ + type: "workflow_variable", + name: item.name, + value: item.value, + id: item.id, + autocomplete: `${item.name.split(" ").join("_")}`, + example: item.value, + }); + } + } - fetchUpdates() - }, 3000) + // FIXME: Add values from previous executions if they exist + if ( + workflow.execution_variables !== null && + workflow.execution_variables !== undefined && + workflow.execution_variables.length > 0 + ) { + for (let varkey in workflow.execution_variables) { + const item = workflow.execution_variables[varkey]; + newActionList.push({ + type: "execution_variable", + name: item.name, + value: item.value, + id: item.id, + autocomplete: `${item.name.split(" ").join("_")}`, + example: "", + }); + } + } - return () => clearInterval(intervalId); - } + var parents = getParents(selectedTrigger); + if (parents.length > 1) { + for (let parentkey in parents) { + const item = parents[parentkey]; + if (item.label === "Runtime Argument") { + continue; + } + + var exampledata = item.example === undefined ? "" : item.example; + // Find previous execution and their variables + if (workflowExecutions.length > 0) { + // Look for the ID + for (let execkey in workflowExecutions) { + if (workflowExecutions[execkey].results === undefined || workflowExecutions[execkey].results === null) { + continue + } + + var foundResult = workflowExecutions[execkey].results.find( + (result) => result.action.id === item.id + ) + if (foundResult === undefined) { + continue + } + + const validated = validateJson(foundResult.result) + if (validated.valid) { + exampledata = validateJson.result + break + } + } + } + + // 1. Take + const actionvalue = { + type: "action", + id: item.id, + name: item.label, + autocomplete: `${item?.label?.split(" ")?.join("_")}`, + example: exampledata, + } + newActionList.push(actionvalue); + } + } + + setSubflowActionList(newActionList); + }, [workflow.workflow_variables, workflow.execution_variables, workflow, selectedTrigger]); + + + useEffect(() => { + if (selectedTriggerIndex === undefined || selectedTriggerIndex === null || selectedTriggerIndex < 0) { + return + } + + //console.log("Failed in trigger selection: ", selectedTriggerIndex, "Trigger: ", selectedTrigger) + + var found = null + try { + for (var key in workflows) { + const curworkflow = workflows[key] + const curtrigger = curworkflow?.triggers[selectedTriggerIndex] + if (curtrigger === undefined || curtrigger === null) { + console.log("Failed in trigger selection (1): ", curworkflow) + continue + } + + if (curtrigger?.parameters === undefined || curtrigger?.parameters === null || curtrigger?.parameters.length === 0) { + console.log("Failed in trigger selection (2): ", curworkflow) + continue + } + + if (curtrigger?.parameters[0] === undefined || curtrigger?.parameters[0] === null || curtrigger?.parameters[0].value === undefined || curtrigger?.parameters[0].value === null) { + console.log("Failed in trigger selection (3): ", curworkflow) + continue + } + + if (curtrigger?.parameters[0].value === selectedTrigger?.parameters[0]?.value) { + found = curworkflow + setSubworkflow(curworkflow) + } + } + + if (found !== null) { + setSubworkflow(found) + } + } catch (e) { + console.log("Failed in trigger selection (4): ", e) + //return + } + + if (found) { + const startNode = found.actions?.find((action) => action.id === workflow?.triggers[selectedTriggerIndex]?.parameters[3]?.value) + setSubworkflowStartnode(startNode) + } + + /* + // Multi-tenant sometimes gives us shit :( + if (selectedTrigger === undefined || selectedTrigger === null || selectedTrigger.id === undefined || selectedTrigger.id === null && selectedTriggerIndex >= 0) { + if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > selectedTriggerIndex) { + setSelectedTrigger(workflow.triggers[selectedTriggerIndex]) + } + } + */ + + // Check if the running state is correct or not according to allTriggers + if (allTriggers !== undefined && allTriggers !== null) { + // Find the active trigger + if (selectedTrigger !== undefined && selectedTrigger !== null && selectedTrigger?.id !== undefined && selectedTrigger?.id !== null) { + + var useTriggers = allTriggers + if (allTriggers.pipelines === undefined || allTriggers.pipelines === null || allTriggers.pipelines.length === 0) { + useTriggers.pipelines = [] + } + + if (allTriggers.schedules === undefined || allTriggers.schedules === null || allTriggers.schedules.length === 0) { + useTriggers.schedules = [] + } + + if (allTriggers.webhooks === undefined || allTriggers.webhooks === null || allTriggers.webhooks.length === 0) { + useTriggers.webhooks = [] + } + + // Check pipelines, schedules and webhooks at once + const allTriggersInOne = useTriggers.pipelines.concat(useTriggers.schedules).concat(useTriggers.webhooks) + for (let triggerkey in allTriggersInOne) { + const curtrigger = allTriggersInOne[triggerkey] + if (curtrigger.id === selectedTrigger.id) { + if (curtrigger.status === undefined || curtrigger.status === null) { + continue + } + + if (curtrigger.running === undefined || curtrigger.running === null) { + continue + } + + var changed = false + if (curtrigger.status !== selectedTrigger.status) { + changed = true + selectedTrigger.status = curtrigger.status + } + + if (curtrigger.running !== selectedTrigger.running) { + changed = true + selectedTrigger.running = curtrigger.running + } + + if (changed) { + //console.log("TRIGGER FIX: ", selectedTrigger) + setSelectedTrigger(selectedTrigger) + } + + break + } + } + } + } + + }, [allTriggers, selectedTrigger, selectedTriggerIndex]) + + useEffect(() => { + // Current variable + future state controlled + // This is so that the loop can stop itself as well + if (loopRunning && loopRunning2) { + const intervalId = setInterval(() => { + if (!loopRunning) { + clearInterval(intervalId); + } + + fetchUpdates() + }, 3000) + + return () => clearInterval(intervalId); + } }, [loopRunning]) // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. @@ -1038,16 +1391,16 @@ const releaseToConnectLabel = "Release to Connect" }) .then((responseJson) => { if (responseJson.success === true) { - if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { - setSelectedMeta(responseJson.meta) - } + if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { + setSelectedMeta(responseJson.meta) + } if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { - const imgRegex = / { - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } + const listOrgCache = (orgId) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } - fetch(`${globalUrl}/api/v1/orgs/${orgId}/list_cache`, { - method: "GET", - headers: headers, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!"); - return; - } + fetch(`${globalUrl}/api/v1/orgs/${orgId}/list_cache`, { + method: "GET", + headers: headers, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } - return response.json(); - }) - .then((responseJson) => { - setListCache(responseJson); - }) - .catch((error) => { - toast(error.toString()); - }); - }; + return response.json(); + }) + .then((responseJson) => { + setListCache(responseJson); + }) + .catch((error) => { + toast(error.toString()); + }); + }; const getWorkflowExecutionCount = (workflowId) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } fetch(`${globalUrl}/api/v1/workflows/${workflowId}/executions/count`, { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -1131,15 +1489,21 @@ const releaseToConnectLabel = "Release to Connect" .catch((error) => { toast(error.toString()); }); - }; + }; const getAvailableWorkflows = (trigger_index) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -1161,27 +1525,27 @@ const releaseToConnectLabel = "Release to Connect" for (let paramkey in trigger.parameters) { const param = trigger.parameters[paramkey]; - // User Input & Subflow nodes + // User Input & Subflow nodes if (param.name === "workflow" || param.name === "subflow") { - const paramIndex = param.name === "workflow" ? 0 : 5 - if (workflow.triggers[trigger_index].parameters[paramIndex].value !== subworkflow.id) { - if (param.value === workflow.id) { - setSubworkflow(workflow); - baseSubflow = workflow - } else { - const sub = responseJson.find((data) => data.id === param.value); - if (sub !== undefined && subworkflow.id !== sub.id) { - baseSubflow = sub - setSubworkflow(sub); - } - } - } - } + const paramIndex = param.name === "workflow" ? 0 : 5 + if (workflow.triggers[trigger_index].parameters[paramIndex].value !== subworkflow?.id) { + if (param.value === workflow?.id) { + setSubworkflow(workflow); + baseSubflow = workflow + } else { + const sub = responseJson.find((data) => data?.id === param.value); + if (sub !== undefined && subworkflow?.id !== sub?.id) { + baseSubflow = sub + setSubworkflow(sub); + } + } + } + } if (param.name === "startnode" && param.value !== undefined && param.value !== null) { - + if (Object.getOwnPropertyNames(baseSubflow).length > 0) { - const foundAction = baseSubflow.actions.find(action => action.id === param.value) + const foundAction = baseSubflow.actions.find(action => action?.id === param.value) if (foundAction !== null && foundAction !== undefined) { setSubworkflowStartnode(foundAction); } @@ -1193,37 +1557,37 @@ const releaseToConnectLabel = "Release to Connect" } } - if (workflows.length === 0) { - //console.log("First request. Checking for parent trigger (if this is subflow") - var parentworkflows = [] - var parent_ids = [] - for (let workflowkey in responseJson) { - const innerworkflow = responseJson[workflowkey] + if (workflows.length === 0) { + //console.log("First request. Checking for parent trigger (if this is subflow") + var parentworkflows = [] + var parent_ids = [] + for (let workflowkey in responseJson) { + const innerworkflow = responseJson[workflowkey] - for (let triggerkey in innerworkflow.triggers) { - const trigger = innerworkflow.triggers[triggerkey] - if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") { + for (let triggerkey in innerworkflow.triggers) { + const trigger = innerworkflow.triggers[triggerkey] + if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") { - for (let paramkey in trigger.parameters) { - const param = trigger.parameters[paramkey] - if ((param.name === "workflow" || param.name === "subflow") && param.value === props.match.params.key && !parent_ids.includes(innerworkflow.id)) { + for (let paramkey in trigger.parameters) { + const param = trigger.parameters[paramkey] + if ((param.name === "workflow" || param.name === "subflow") && param.value === props.match.params.key && !parent_ids.includes(innerworkflow?.id)) { - parent_ids.push(innerworkflow.id) - parentworkflows.push({ - id: innerworkflow.id, - name: innerworkflow.name, - image: innerworkflow.image, - }) - } - } - } - } - } + parent_ids.push(innerworkflow?.id) + parentworkflows.push({ + id: innerworkflow?.id, + name: innerworkflow.name, + image: innerworkflow.image, + }) + } + } + } + } + } - if (parentworkflows.length > 0) { - setParentWorkflows(parentworkflows.filter(wf => wf.id !== props.match.params.key)) - } - } + if (parentworkflows.length > 0) { + setParentWorkflows(parentworkflows.filter(wf => wf?.id !== props.match.params.key)) + } + } setWorkflows(responseJson); } @@ -1256,74 +1620,15 @@ const releaseToConnectLabel = "Release to Connect" ); } - const generateApikey = () => { - fetch(globalUrl + "/api/v1/generateapikey", { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for APIKEY gen :O!"); - } - - return response.json(); - }) - .then((responseJson) => { - setUserSettings(responseJson); - }) - .catch((error) => { - console.log("Apikey error: ", error); - }); - }; - - 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 for get settings :O!"); - } - - return response.json(); - }) - .then((responseJson) => { - if ( - responseJson.success === true && - (responseJson.apikey === undefined || - responseJson.apikey.length === 0 || - responseJson.apikey === null) - ) { - generateApikey(); - } - - if (responseJson.success === true) { - setUserSettings(responseJson) - } - }) - .catch((error) => { - console.log("Settings error: ", error); - }); - }; - const setNewAppAuth = (appAuthData, refresh) => { - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { - headers["Org-Id"] = workflow.org_id - } + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } fetch(globalUrl + "/api/v1/apps/authentication", { method: "PUT", @@ -1333,41 +1638,41 @@ const releaseToConnectLabel = "Release to Connect" }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for setting app auth :O!"); + console.log("Status not 200 for setting app auth :O!"); - if (response.status === 400) { - toast.error("Failed setting new auth. Please try again", { - "autoClose": true, - }) - } - } + if (response.status === 400) { + toast.error("Failed setting new auth. Please try again", { + "autoClose": true, + }) + } + } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { - // Remove the timeout. Has to be clicked + // Remove the timeout. Has to be clicked toast.error("Error: " + responseJson.reason, { - "autoClose": false, - }) + "autoClose": false, + }) } else { - if (refresh === true) { - getAppAuthentication(true, true, true) - } else { - getAppAuthentication(true, false) - } + if (refresh === true) { + getAppAuthentication(true, true, true) + } else { + getAppAuthentication(true, false) + } setAuthenticationModalOpen(false) // Needs a refresh with the new authentication.. //toast("Successfully saved new app auth") - if (configureWorkflowModalOpen === true) { - setConfigureWorkflowModalOpen(false) + if (configureWorkflowModalOpen === true) { + setConfigureWorkflowModalOpen(false) - setTimeout(() => { - setConfigureWorkflowModalOpen(true) - }, 1000) - } + setTimeout(() => { + setConfigureWorkflowModalOpen(true) + }, 1000) + } } }) .catch((error) => { @@ -1376,37 +1681,51 @@ const releaseToConnectLabel = "Release to Connect" }); }; - const getWorkflowExecution = (id, execution_id, filter) => { - var url = `${globalUrl}/api/v2/workflows/${id}/executions` - var method = "GET" - if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") { + const getWorkflowExecution = (id, execution_id, filter, orgId) => { + var url = `${globalUrl}/api/v2/workflows/${id}/executions` + var method = "GET" + if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") { - // Check for - if (executionFilter !== undefined && executionFilter !== null && executionFilter.length > 0) { - filter = executionFilter - } else { - filter = "ALL" - } - } + // Check for + if (executionFilter !== undefined && executionFilter !== null && executionFilter.length > 0 && filter !== "ALL") { + filter = executionFilter + } else { + filter = "ALL" + } + } - var formattedBody = { + let headers = { + "Content-Type": "application/json", + Accept: "application/json", + } + + if (currentWorkflow?.id?.length > 0 && currentWorkflow?.id !== undefined && currentWorkflow?.id !== null) { + id = currentWorkflow.id; + } + + if (currentWorkflow?.org_id?.length > 0 && currentWorkflow?.org_id !== undefined && currentWorkflow?.org_id !== null) { + headers["Org-Id"] = currentWorkflow.org_id; + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + + var formattedBody = { method: method, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", } - if (filter !== "ALL") { - formattedBody.method = "POST" + if (filter !== "ALL") { + formattedBody.method = "POST" - formattedBody.body = JSON.stringify({ - "status": filter, - "workflow_id": id, - }) + formattedBody.body = JSON.stringify({ + "status": filter, + "workflow_id": id, + }) - url = `${globalUrl}/api/v1/workflows/search` + url = `${globalUrl}/api/v1/workflows/search` } @@ -1420,8 +1739,8 @@ const releaseToConnectLabel = "Release to Connect" }) .then((responseJson) => { if (responseJson !== undefined && responseJson !== null && responseJson.runs !== undefined && responseJson.runs !== null) { - responseJson.executions = responseJson.runs - } + responseJson.executions = responseJson.runs + } if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) { @@ -1435,24 +1754,24 @@ const releaseToConnectLabel = "Release to Connect" tmpView = execution_id; } - // Compare with currently selected item + // Compare with currently selected item if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { - // Don't clean up if it's already open - if (executionModalOpen === true) { - return - } + // Don't clean up if it's already open + if (executionModalOpen === true) { + return + } const execution = responseJson.executions.find((data) => data.execution_id === tmpView); - setExecutionModalOpen(true) + setExecutionModalOpen(true) if (execution !== null && execution !== undefined) { - if (execution.execution_argument.includes("too large")) { - setExecutionData({}); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - setExecutionData(execution); - } + if (execution.execution_argument.includes("too large")) { + setExecutionData({}); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + setExecutionData(execution); + } setExecutionModalView(1); setExecutionRequest({ @@ -1472,7 +1791,7 @@ const releaseToConnectLabel = "Release to Connect" execution_id: tmpView, //authorization: data.authorization, } - setExecutionRunning(true); + setExecutionRunning(true); setExecutionModalView(1); setExecutionRequest(cur_execution); start(); @@ -1489,14 +1808,14 @@ const releaseToConnectLabel = "Release to Connect" var tmpView = new URLSearchParams(cursearch).get("execution_id"); if (tmpView === undefined || tmpView === null || tmpView.length === 0) { const execution_id = tmpView; - setExecutionModalView(1); - setExecutionRequest({ - execution_id: execution_id, - }); + setExecutionModalView(1); + setExecutionRequest({ + execution_id: execution_id, + }); - start() + start() } - } + } }) .catch((error) => { //toast(error.toString()); @@ -1505,26 +1824,32 @@ const releaseToConnectLabel = "Release to Connect" }; const fetchUpdates = () => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + fetch(globalUrl + "/api/v1/streams/results", { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, body: JSON.stringify(executionRequest), credentials: "include", - cors: "no-cors", + cors: "no-cors", }) .then((response) => { if (response.status !== 200) { stop(); - setExecutionModalView(0); - //toast("Failed loading the workflow run") + setExecutionModalView(0); + //toast("Failed loading the workflow run") console.log("Status not 200 for stream results :O!"); - //const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - //const newitem = removeParam("execution_id", cursearch); - //navigate(curpath + newitem) + //const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + //const newitem = removeParam("execution_id", cursearch); + //navigate(curpath + newitem) } return response.json(); @@ -1541,13 +1866,19 @@ const releaseToConnectLabel = "Release to Connect" const abortExecution = () => { setExecutionRunning(false); - fetch(globalUrl +"/api/v1/workflows/" +props.match.params.key +"/executions/" +executionRequest.execution_id +"/abort", + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + fetch(globalUrl + "/api/v1/workflows/" + props.match.params.key + "/executions/" + executionRequest.execution_id + "/abort", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", } ) @@ -1569,12 +1900,12 @@ const releaseToConnectLabel = "Release to Connect" toast("Unable to save the configuration"); return; } - + trigger.parameters = [] const command = document.getElementById('sigma')?.value - if(command) { + if (command) { trigger.parameters.push({ name: "command", value: command @@ -1583,14 +1914,14 @@ const releaseToConnectLabel = "Release to Connect" toast("Please enter the comamnd"); return; } - + // if (autoOffsetReset) { // trigger.parameters.push({ // name: "auto_offset_reset", // value: autoOffsetReset // }); // } - + setTenzirConfigModalOpen(false); }; @@ -1599,174 +1930,174 @@ const releaseToConnectLabel = "Release to Connect" toast("Unable to save the configuration"); return; } - if (selectedOption === "Kafka Queue") { - trigger.parameters = [] + if (selectedOption === "Kafka Queue") { + trigger.parameters = [] - const pipeline = document.getElementById('pipeline')?.value - - setTenzirConfigModalOpen(false); - } else if (selectedOption === "Syslog listener") { - trigger.parameters = [] + const pipeline = document.getElementById('pipeline')?.value - const endpoint = document.getElementById('endpoint')?.value + setTenzirConfigModalOpen(false); + } else if (selectedOption === "Syslog listener") { + trigger.parameters = [] - if(endpoint) { - trigger.parameters.push({ - name: "endpoint", - value: endpoint - }) - } else { - toast("Please enter your endpoint"); - return; + const endpoint = document.getElementById('endpoint')?.value + + if (endpoint) { + trigger.parameters.push({ + name: "endpoint", + value: endpoint + }) + } else { + toast("Please enter your endpoint"); + return; + } } - } }; - - - const handleColoring = (actionId, status, label) => { - if (cy === undefined) { - return - } - var currentnode = cy.getElementById(actionId); - if (currentnode.length === 0) { - return - //continue; - } - currentnode = currentnode[0]; - const outgoingEdges = currentnode.outgoers("edge"); - const incomingEdges = currentnode.incomers("edge"); + const handleColoring = (actionId, status, label) => { + if (cy === undefined) { + return + } - switch (status) { - case "EXECUTING": - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - incomingEdges.addClass("success-highlight"); - currentnode.addClass("executing-highlight"); - break; - case "SKIPPED": - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - currentnode.removeClass("executing-highlight"); - currentnode.addClass("skipped-highlight"); - break; - case "WAITING": - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - currentnode.addClass("executing-highlight"); + var currentnode = cy.getElementById(actionId); + if (currentnode.length === 0) { + return + //continue; + } - if (!visited.includes(label)) { - if (executionRunning) { - visited.push(label); - setVisited(visited); - } - } + currentnode = currentnode[0]; + const outgoingEdges = currentnode.outgoers("edge"); + const incomingEdges = currentnode.incomers("edge"); - // FIXME - add outgoing nodes to executing - //const outgoingNodes = outgoingEdges.find().data().target - if (outgoingEdges.length > 0) { - outgoingEdges.addClass("success-highlight"); - } - break; - case "SUCCESS": - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("executing-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - currentnode.addClass("success-highlight"); - incomingEdges.addClass("success-highlight"); - outgoingEdges.addClass("success-highlight"); + switch (status) { + case "EXECUTING": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + incomingEdges.addClass("success-highlight"); + currentnode.addClass("executing-highlight"); + break; + case "SKIPPED": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.addClass("skipped-highlight"); + break; + case "WAITING": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("executing-highlight"); - if (visited !== undefined && visited !== null && !visited.includes(label)) { - if (executionRunning) { - visited.push(label); - setVisited(visited); - } - } + if (!visited.includes(label)) { + if (executionRunning) { + visited.push(label); + setVisited(visited); + } + } - // FIXME - add outgoing nodes to executing - //const outgoingNodes = outgoingEdges.find().data().target - if (outgoingEdges.length > 0) { - for (let i = 0; i < outgoingEdges.length; i++) { - const edge = outgoingEdges[i]; - const targetnode = cy.getElementById(edge.data().target); - if ( - targetnode !== undefined && - !targetnode.classes().includes("success-highlight") && - !targetnode.classes().includes("failure-highlight") - ) { - targetnode.removeClass("not-executing-highlight"); - targetnode.removeClass("success-highlight"); - targetnode.removeClass("shuffle-hover-highlight"); - targetnode.removeClass("failure-highlight"); - targetnode.removeClass("awaiting-data-highlight"); - targetnode.addClass("executing-highlight"); - } - } - } - break; - case "FAILURE": - //When status comes as failure, allow user to start workflow execution - if (executionRunning) { - setExecutionRunning(false); - } + // FIXME - add outgoing nodes to executing + //const outgoingNodes = outgoingEdges.find().data().target + if (outgoingEdges.length > 0) { + outgoingEdges.addClass("success-highlight"); + } + break; + case "SUCCESS": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("success-highlight"); + incomingEdges.addClass("success-highlight"); + outgoingEdges.addClass("success-highlight"); - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.addClass("failure-highlight"); + if (visited !== undefined && visited !== null && !visited.includes(label)) { + if (executionRunning) { + visited.push(label); + setVisited(visited); + } + } - if (!visited.includes(label)) { - //if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { - // toast("Error for " + item.action.label + " with result " + item.result); - //} - visited.push(label); - setVisited(visited); - } - break; - case "AWAITING_DATA": - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.addClass("awaiting-data-highlight"); - break; - default: - currentnode.removeClass("not-executing-highlight"); - currentnode.removeClass("executing-highlight"); - currentnode.removeClass("success-highlight"); - currentnode.removeClass("failure-highlight"); - currentnode.removeClass("shuffle-hover-highlight"); - currentnode.removeClass("awaiting-data-highlight"); - currentnode.addClass("not-executing-highlight"); - //console.log("DEFAULT -> Clearing!"); - break; - } - } + // FIXME - add outgoing nodes to executing + //const outgoingNodes = outgoingEdges.find().data().target + if (outgoingEdges.length > 0) { + for (let i = 0; i < outgoingEdges.length; i++) { + const edge = outgoingEdges[i]; + const targetnode = cy.getElementById(edge.data().target); + if ( + targetnode !== undefined && + !targetnode.classes().includes("success-highlight") && + !targetnode.classes().includes("failure-highlight") + ) { + targetnode.removeClass("not-executing-highlight"); + targetnode.removeClass("success-highlight"); + targetnode.removeClass("shuffle-hover-highlight"); + targetnode.removeClass("failure-highlight"); + targetnode.removeClass("awaiting-data-highlight"); + targetnode.addClass("executing-highlight"); + } + } + } + break; + case "FAILURE": + //When status comes as failure, allow user to start workflow execution + if (executionRunning) { + setExecutionRunning(false); + } + + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.addClass("failure-highlight"); + + if (!visited.includes(label)) { + //if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { + // toast("Error for " + item.action.label + " with result " + item.result); + //} + visited.push(label); + setVisited(visited); + } + break; + case "AWAITING_DATA": + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.addClass("awaiting-data-highlight"); + break; + default: + currentnode.removeClass("not-executing-highlight"); + currentnode.removeClass("executing-highlight"); + currentnode.removeClass("success-highlight"); + currentnode.removeClass("failure-highlight"); + currentnode.removeClass("shuffle-hover-highlight"); + currentnode.removeClass("awaiting-data-highlight"); + currentnode.addClass("not-executing-highlight"); + //console.log("DEFAULT -> Clearing!"); + break; + } + } // Controls the colors and direction of execution results. // Style is in defaultCytoscapeStyle.js const handleUpdateResults = (responseJson, executionRequest) => { - if (responseJson === undefined || responseJson === null || responseJson.success === false) { - stop() - return - } -//console.log(responseJson) + if (responseJson === undefined || responseJson === null || responseJson.success === false) { + stop() + return + } + //console.log(responseJson) // Loop nodes and find results // Update on every interval? idk @@ -1776,15 +2107,15 @@ const releaseToConnectLabel = "Release to Connect" // Doesn't work because this is some async garbage if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) { - //console.log("Updating data!") + //console.log("Updating data!") setExecutionData(responseJson) } else { - if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING" || responseJson.status === "FINISHED") { - stop() - } + if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING" || responseJson.status === "FINISHED") { + stop() + } //console.log("NOT updating executiondata state."); - return + return } } } @@ -1794,21 +2125,21 @@ const releaseToConnectLabel = "Release to Connect" return; } - if (responseJson.results !== null && responseJson.results.length > 0) { - // First clear current nodes - if (responseJson.workflow.actions !== undefined && responseJson.workflow.actions !== null) { - // In clearing of actions - for (let actionKey in responseJson.workflow.actions) { - var item = responseJson.workflow.actions[actionKey]; + if (responseJson.results !== null && responseJson.results.length > 0) { + // First clear current nodes + if (responseJson.workflow.actions !== undefined && responseJson.workflow.actions !== null) { + // In clearing of actions + for (let actionKey in responseJson.workflow.actions) { + var item = responseJson.workflow.actions[actionKey]; - handleColoring(item.id, "", item.label) - } - } + handleColoring(item.id, "", item.label) + } + } - for (let resultKey in responseJson.results) { - var item = responseJson.results[resultKey]; + for (let resultKey in responseJson.results) { + var item = responseJson.results[resultKey]; - handleColoring(item.action.id, item.status, item.action.label) + handleColoring(item.action.id, item.status, item.action.label) } } @@ -1819,15 +2150,15 @@ const releaseToConnectLabel = "Release to Connect" setExecutionRunning(false); } - var curelements = cy.elements(); - for (let i = 0; i < curelements.length; i++) { - if (curelements[i].classes().includes("executing-highlight")) { - curelements[i].removeClass("executing-highlight"); - curelements[i].addClass("failure-highlight"); - } - } + var curelements = cy.elements(); + for (let i = 0; i < curelements.length; i++) { + if (curelements[i].classes().includes("executing-highlight")) { + curelements[i].removeClass("executing-highlight"); + curelements[i].addClass("failure-highlight"); + } + } - getWorkflowExecution(props.match.params.key, ""); + getWorkflowExecution(props.match.params.key, "", executionFilter); } else if (responseJson.status === "FINISHED") { setExecutionRunning(false); stop(); @@ -1841,37 +2172,43 @@ const releaseToConnectLabel = "Release to Connect" const sendStreamRequest = (body) => { //console.log("Stream not activated yet.") if (!isCloud) { - return + return } - if (streamDisabled) { - return - } + if (streamDisabled) { + return + } // Session may be important here huh body.user_id = userdata.id - //const url = ${globalUrl}/api/v1/workflows/${props.match.params.key}/stream - //const streamUrl = "http://localhost:5002" + //const url = ${globalUrl}/api/v1/workflows/${props.match.params.key}/stream + //const streamUrl = "http://localhost:5002" - //console.log("Stream request: ", body) - const streamUrl = "https://stream.shuffler.io" - const url = `${streamUrl}/api/v1/workflows/${props.match.params.key}/stream` + //console.log("Stream request: ", body) + const streamUrl = "https://stream.shuffler.io" + const url = `${streamUrl}/api/v1/workflows/${props.match.params.key}/stream` - var parsedbody = body - try { - parsedbody = JSON.stringify(body) - } catch (e) { - console.log("Error parsing body for stream: ", e) - } + var parsedbody = body + try { + parsedbody = JSON.stringify(body) + } catch (e) { + console.log("Error parsing body for stream: ", e) + } + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } fetch(url, { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, body: parsedbody, credentials: "include", }) @@ -1879,8 +2216,8 @@ const releaseToConnectLabel = "Release to Connect" setSavingState(0); if (response.status !== 200) { - setStreamDisabled(true) - streamDisabled2 = true + setStreamDisabled(true) + streamDisabled2 = true //console.log("Status not 200 for stream :O!"); } @@ -1891,35 +2228,35 @@ const releaseToConnectLabel = "Release to Connect" }) .catch((error) => { console.log("Stream send error: ", error.toString()) - setStreamDisabled(true) - streamDisabled2 = true + setStreamDisabled(true) + streamDisabled2 = true }) } - const saveWorkflow = (curworkflow, executionArgument, startNode, duplicationOrg) => { + const saveWorkflow = (curworkflow, executionArgument, startNode, duplicationOrg, skip_popup) => { var success = false; if (isCloud && !isLoggedIn) { console.log("Should redirect to register with redirect.") - setTimeout(() => { - toast("You may not have access to this workflow.") - //window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` - window.location.href = `/workflows` - }, 2500) - + setTimeout(() => { + toast("You may not have access to this workflow.") + //window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + window.location.href = `/workflows` + }, 2500) + return } - if (curworkflow === undefined || curworkflow === null) { - console.log("No workflow during save") - return - } + if (curworkflow === undefined || curworkflow === null) { + console.log("No workflow during save") + return + } - if (curworkflow.actions === undefined || curworkflow.actions === null || curworkflow.actions.length === 0) { - console.log("Can't save without actions") - return - } + if (curworkflow.actions === undefined || curworkflow.actions === null || curworkflow.actions.length === 0) { + //toast.error("The workflow is empty. Please add at least one action.") + return + } setSavingState(2); @@ -1931,116 +2268,136 @@ const releaseToConnectLabel = "Release to Connect" useworkflow = curworkflow; } - - + + var cyelements = [] - if (cy !== undefined && cy !== null) { - cyelements = cy.elements() - } + if (cy !== undefined && cy !== null) { + cyelements = cy.elements() + } - var newActions = []; - var newTriggers = []; - var newBranches = []; - var newVBranches = []; - var newComments = []; - for (let cyelementsKey in cyelements) { - if (cyelements[cyelementsKey].data === undefined) { - continue; - } + var newActions = []; + var newTriggers = []; + var newBranches = []; + var newVBranches = []; + var newComments = []; + for (let cyelementsKey in cyelements) { + if (cyelements[cyelementsKey].data === undefined) { + continue; + } - var type = cyelements[cyelementsKey].data()["type"] - if (type === undefined) { - if (cyelements[cyelementsKey].data().source === undefined || cyelements[cyelementsKey].data().target === undefined) { - continue - } + var type = cyelements[cyelementsKey].data()["type"] + if (type === undefined) { + if (cyelements[cyelementsKey].data().source === undefined || cyelements[cyelementsKey].data().target === undefined) { + continue + } - // Get the parent item - var source_attachment = "" - const branchSource = cy.getElementById(cyelements[cyelementsKey].data().source) - if (branchSource === undefined || branchSource === null) { - } else { - const branchSourceData = branchSource.data() - if (branchSourceData !== undefined && branchSourceData !== null && branchSourceData.attachedTo !== undefined) { - source_attachment = branchSourceData.attachedTo + // Get the parent item + var source_attachment = "" + const branchSource = cy.getElementById(cyelements[cyelementsKey].data().source) + if (branchSource === undefined || branchSource === null) { + } else { + const branchSourceData = branchSource.data() + if (branchSourceData !== undefined && branchSourceData !== null && branchSourceData.attachedTo !== undefined) { + source_attachment = branchSourceData.attachedTo - // Check if it's the 'else' or not based on uuidv5 - const else_attachment = uuidv5(source_attachment, uuidv5.URL) - if (else_attachment === branchSourceData.id) { - source_attachment = source_attachment+"-else" - } + // Check if it's the 'else' or not based on uuidv5 + const else_attachment = uuidv5(source_attachment, uuidv5.URL) + if (else_attachment === branchSourceData.id) { + source_attachment = source_attachment + "-else" + } - console.log("Source parent: ", source_attachment) - } - } + console.log("Source parent: ", source_attachment) + } + } - var parsedElement = { - id: cyelements[cyelementsKey].data().id, - source_id: cyelements[cyelementsKey].data().source, - destination_id: cyelements[cyelementsKey].data().target, - conditions: cyelements[cyelementsKey].data().conditions, - decorator: cyelements[cyelementsKey].data().decorator, + var parsedElement = { + id: cyelements[cyelementsKey].data().id, + source_id: cyelements[cyelementsKey].data().source, + destination_id: cyelements[cyelementsKey].data().target, + conditions: cyelements[cyelementsKey].data().conditions, + decorator: cyelements[cyelementsKey].data().decorator, - source_parent: source_attachment, - } + source_parent: source_attachment, + } - if (parsedElement.decorator) { - newVBranches.push(parsedElement) - } else { - newBranches.push(parsedElement) - } + if (parsedElement.decorator) { + newVBranches.push(parsedElement) + } else { + newBranches.push(parsedElement) + } - } else { - if (type === "ACTION") { - const cyelement = cyelements[cyelementsKey].data(); - const elementid = - cyelement.id === undefined || cyelement.id === null - ? cyelement["_id"] - : cyelement.id; + } else { + if (type === "ACTION") { + const cyelement = cyelements[cyelementsKey].data() + const elementid = + cyelement.id === undefined || cyelement.id === null + ? cyelement["_id"] + : cyelement.id; - var curworkflowAction = useworkflow.actions.find( - (a) => - a !== undefined && - (a["id"] === elementid || a["_id"] === elementid) - ); - if (curworkflowAction === undefined) { - curworkflowAction = cyelements[cyelementsKey].data(); - } + var curworkflowAction = useworkflow.actions.find((a) => a !== undefined && (a["id"] === elementid || a["_id"] === elementid)) - curworkflowAction.position = cyelements[cyelementsKey].position(); + if (curworkflowAction === undefined) { + curworkflowAction = cyelements[cyelementsKey].data() + } + + curworkflowAction.position = cyelements[cyelementsKey].position(); // workaround to fix some edgecases - if ( - curworkflowAction.parameters === "" || - curworkflowAction.parameters === null - ) { - curworkflowAction.parameters = []; - } + if (curworkflowAction.parameters === "" || curworkflowAction.parameters === null) { + curworkflowAction.parameters = []; + } - if ( - curworkflowAction.example === undefined || - curworkflowAction.example === "" || - curworkflowAction.example === null - ) { - if (cyelements[cyelementsKey].data().example !== undefined) { - curworkflowAction.example = cyelements[cyelementsKey].data().example; - } - } + if ( + curworkflowAction.example === undefined || + curworkflowAction.example === "" || + curworkflowAction.example === null + ) { + if (cyelements[cyelementsKey].data().example !== undefined) { + curworkflowAction.example = cyelements[cyelementsKey].data().example; + } + } // Override just in this place + //console.log("WORKFLOWACTION: ", curworkflowAction, "CY: ", cyelements[cyelementsKey].data()) + const foundLabel = cyelements[cyelementsKey].data("label") + if (foundLabel !== undefined && foundLabel !== null && foundLabel.length > 0) { + curworkflowAction.label = foundLabel + } + + const foundAuth = cyelements[cyelementsKey].data("authentication_id") + if (foundAuth !== undefined && foundAuth !== null && foundAuth.length > 0) { + curworkflowAction.authentication_id = foundAuth + } + + const executionDelay = cyelements[cyelementsKey].data("execution_delay") + if (executionDelay !== undefined && executionDelay !== null && executionDelay.length > 0) { + curworkflowAction.execution_delay = executionDelay + } + + const executionVariable = cyelements[cyelementsKey].data("execution_variable") + if (executionVariable !== undefined && executionVariable !== null) { + curworkflowAction.execution_variable = executionVariable + } + + const cyParams = cyelements[cyelementsKey].data("parameters") + if (cyParams !== undefined && cyParams !== null && cyParams.length > 0) { + curworkflowAction.parameters = cyParams + } + curworkflowAction.errors = []; curworkflowAction.isValid = true; - // Cleans up OpenAPI items - var newparams = []; - for (let parametersKey in curworkflowAction.parameters) { - const thisitem = curworkflowAction.parameters[parametersKey]; - if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { - continue; - } + // Cleans up OpenAPI items + var newparams = []; + for (let parametersKey in curworkflowAction.parameters) { + const thisitem = curworkflowAction.parameters[parametersKey]; + if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { + continue; + } - if (thisitem.value !== undefined && thisitem.value !== null && Array.isArray(thisitem.value)) { - thisitem.value = thisitem.value.join(",") - } + if (thisitem.value !== undefined && thisitem.value !== null && Array.isArray(thisitem.value)) { + thisitem.value = thisitem.value.join(",") + } newparams.push(thisitem); } @@ -2052,17 +2409,17 @@ const releaseToConnectLabel = "Release to Connect" useworkflow.triggers = []; } - var curworkflowTrigger = useworkflow.triggers.find( - (a) => a.id === cyelements[cyelementsKey].data()["id"] - ); - if (curworkflowTrigger === undefined) { - curworkflowTrigger = cyelements[cyelementsKey].data(); - } + var curworkflowTrigger = useworkflow.triggers.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ); + if (curworkflowTrigger === undefined) { + curworkflowTrigger = cyelements[cyelementsKey].data(); + } - curworkflowTrigger.position = cyelements[cyelementsKey].position(); - if (curworkflowTrigger.canConnect === false) { - continue - } + curworkflowTrigger.position = cyelements[cyelementsKey].position(); + if (curworkflowTrigger.canConnect === false) { + continue + } newTriggers.push(curworkflowTrigger); } else if (type === "COMMENT") { @@ -2070,17 +2427,17 @@ const releaseToConnectLabel = "Release to Connect" useworkflow.comments = []; } - var curworkflowComment = useworkflow.comments.find( - (a) => a.id === cyelements[cyelementsKey].data()["id"] - ) + var curworkflowComment = useworkflow.comments.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ) - if (curworkflowComment === undefined) { - curworkflowComment = cyelements[cyelementsKey].data(); - try { - curworkflowComment.position.x = parseInt(curworkflowComment.position.x) - } catch (e) { - console.log("Failed to parse position Y of comment: ", curworkflowComment.position.x) - } + if (curworkflowComment === undefined) { + curworkflowComment = cyelements[cyelementsKey].data(); + try { + curworkflowComment.position.x = parseInt(curworkflowComment.position.x) + } catch (e) { + console.log("Failed to parse position Y of comment: ", curworkflowComment.position.x) + } try { curworkflowComment.position.y = parseInt(curworkflowComment.position.y) @@ -2103,8 +2460,8 @@ const releaseToConnectLabel = "Release to Connect" curworkflowComment.width = 200 } - curworkflowComment.position = cyelements[cyelementsKey].position(); - //console.log(curworkflowComment) + curworkflowComment.position = cyelements[cyelementsKey].position(); + //console.log(curworkflowComment) newComments.push(curworkflowComment); } else { @@ -2127,18 +2484,18 @@ const releaseToConnectLabel = "Release to Connect" useworkflow.errors = []; useworkflow.previously_saved = true; - // Find the startnode in actions - /* - var foundStartNode = useworkflow.actions.find((a) => a.is_start_node === true) - console.log("Discovered startnode: ", foundStartNode) - if ((foundStartNode === undefined || foundStartNode === null) && useworkflow.actions.length > 0) { - // Set a startnode - useworkflow.actions[0].is_start_node = true - useworkflow.start = useworkflow.actions[0].id - } - */ + // Find the startnode in actions + /* + var foundStartNode = useworkflow.actions.find((a) => a.is_start_node === true) + console.log("Discovered startnode: ", foundStartNode) + if ((foundStartNode === undefined || foundStartNode === null) && useworkflow.actions.length > 0) { + // Set a startnode + useworkflow.actions[0].is_start_node = true + useworkflow.start = useworkflow.actions[0].id + } + */ - if (cy !== undefined) { + if (cy !== undefined && cy !== null) { // scale: 0.3, // bg: "#27292d", const cyImageData = cy.png({ @@ -2152,25 +2509,25 @@ const releaseToConnectLabel = "Release to Connect" } } - if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { - useworkflow.id = props.match.params.key - } - - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", + if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { + useworkflow.id = props.match.params.key } - if (useworkflow.org_id !== undefined && useworkflow.org_id !== null && useworkflow.org_id.length > 0) { - headers["Org-Id"] = useworkflow.org_id - } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - // Realtime makes the workflow if it doesn't exist - /* - if (duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { - headers["Org-Id"] = duplicationOrg - } - */ + if (useworkflow.org_id !== undefined && useworkflow.org_id !== null && useworkflow.org_id.length > 0) { + headers["Org-Id"] = useworkflow.org_id + } + + // Realtime makes the workflow if it doesn't exist + /* + if (duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { + headers["Org-Id"] = duplicationOrg + } + */ setLastSaved(true); fetch(`${globalUrl}/api/v1/workflows/${useworkflow.id}`, { @@ -2183,30 +2540,36 @@ const releaseToConnectLabel = "Release to Connect" if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); } else { - if (distributedFromParent === "" && suborgWorkflows === []) { - } else { - getChildWorkflows(useworkflow.id) - } - } + if (distributedFromParent === "" && suborgWorkflows === []) { + } else { + // Slight delay to ensure we are not too fast compared to backend goroutines. + getChildWorkflows(useworkflow.id) + setTimeout(() => { + getChildWorkflows(useworkflow.id) + },250) + } + } return response.json(); }) .then((responseJson) => { - if (useworkflow.id === originalWorkflow.id && duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { - //duplicateParentWorkflow(useworkflow, duplicationOrg, true) - duplicateParentWorkflow(useworkflow, duplicationOrg, true) - } + if (useworkflow.id === originalWorkflow.id && duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { + //duplicateParentWorkflow(useworkflow, duplicationOrg, true) + duplicateParentWorkflow(useworkflow, duplicationOrg, true) + } if (executionArgument !== undefined && startNode !== undefined) { //console.log("Running execution AFTER saving"); setSavingState(0); - executeWorkflow(executionArgument, startNode, true); + executeWorkflow(executionArgument, startNode, true, skip_popup); return; } + if (!responseJson.success) { + setSavingState(0); - console.log(responseJson); + console.log("Workflow failed loading: ", responseJson); if (responseJson.reason !== undefined && responseJson.reason !== null) { toast("Failed to save: " + responseJson.reason); } else { @@ -2235,78 +2598,78 @@ const releaseToConnectLabel = "Release to Connect" workflow.isValid = true; workflow.is_valid = true; - const cyelements = cy.elements(); - - for (let i = 0; i < cyelements.length; i++) { - //cyelements[i].removeStyle(); - cyelements[i].data().is_valid = true; - cyelements[i].data().errors = []; - } + const cyelements = cy.elements(); - for (let actionkey in workflow.actions) { - workflow.actions[actionkey].is_valid = true; - workflow.actions[actionkey].errors = []; - } - } else { - responseJson.errors.map((error) => { - // Find the word itself - const wordsplit = error.split(" ") - if (wordsplit.length < 2) { - return - } + for (let i = 0; i < cyelements.length; i++) { + //cyelements[i].removeStyle(); + cyelements[i].data().is_valid = true; + cyelements[i].data().errors = []; + } - var word = "" - var isaction = false + for (let actionkey in workflow.actions) { + workflow.actions[actionkey].is_valid = true; + workflow.actions[actionkey].errors = []; + } + } else { + responseJson.errors.map((error) => { + // Find the word itself + const wordsplit = error.split(" ") + if (wordsplit.length < 2) { + return + } - for (var mapkey in wordsplit) { - const inword = wordsplit[mapkey] - if (isaction === true) { - word = inword - break - } + var word = "" + var isaction = false - if (inword.toLowerCase() === "action") { - isaction = true - } - } + for (var mapkey in wordsplit) { + const inword = wordsplit[mapkey] + if (isaction === true) { + word = inword + break + } - if (word === "") { - return - } + if (inword.toLowerCase() === "action") { + isaction = true + } + } - const foundnode = cy.nodes().find((node) => { - const nodelabel = node.data("label") - if (nodelabel === undefined || nodelabel === null) { - return false - } + if (word === "") { + return + } - return nodelabel.toLowerCase() === word.toLowerCase() - }) + const foundnode = cy.nodes().find((node) => { + const nodelabel = node.data("label") + if (nodelabel === undefined || nodelabel === null) { + return false + } - if (foundnode !== undefined && foundnode !== null) { - foundnode.data("is_valid", false) + return nodelabel.toLowerCase() === word.toLowerCase() + }) - // FIXME: Maybe append? - foundnode.data("errors", [error]) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("is_valid", false) - const parsedStyle = { - "border-width": "10px", - "border-opacity": ".9", - "border-color": red, - } + // FIXME: Maybe append? + foundnode.data("errors", [error]) - const animationDuration = 150 - foundnode.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } - }) - } + const parsedStyle = { + "border-width": "10px", + "border-opacity": ".9", + "border-color": red, + } + + const animationDuration = 150 + foundnode.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + }) + } setWorkflow(workflow) } @@ -2314,24 +2677,60 @@ const releaseToConnectLabel = "Release to Connect" setTimeout(() => { setSavingState(0); }, 1500); - getRevisionHistory(useworkflow.id) + getRevisionHistory(useworkflow.id, 50, 0, useworkflow.org_id) } }) .catch((error) => { setSavingState(0); - setExecutionRequestStarted(false) + setExecutionRequestStarted(false) console.log("Save workflow error: ", error.toString()); - toast.warn("Failed to save the workflow. Is the network down?") + toast.warn("Failed to save the workflow. Is the network down?") }); - if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { - setOriginalWorkflow(useworkflow) - } + if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { + setOriginalWorkflow(useworkflow) + } return success }; + const fixExample = (input, required) => { + if (input === undefined || input === null || input.length === 0) { + return "" + } + + + // 1. Find anything matching ${variable} + // 2. If it is required, replace it with REQUIRED + // 3. If it is not required, replace it with empty + + // Check if it is a string or not + var newExample = input + if (typeof newExample !== "string") { + return newExample + } + + const found = newExample.match(/\${(.*?)}/g) + if (found === null || found === undefined || found.length === 0) { + return newExample + } + + /* + for (var i = 0; i < found.length; i++) { + //newExample = newExample.replace(found[i], "REQUIRED") + newExample = newExample.replace(found[i], "REPLACE_ME") + } + */ + + return newExample + } + const monitorUpdates = () => { + if (cy === undefined || cy === null) { + console.log("No cy found to verify startnode.") + return true + } + var firstnode = cy.getElementById(workflow.start); if (firstnode.length === 0) { var found = false; @@ -2339,7 +2738,7 @@ const releaseToConnectLabel = "Release to Connect" if (workflow.actions[actionkey].isStartNode) { console.log("Updating startnode"); workflow.start = workflow.actions[actionkey].id; - firstnode = cy.getElementById(workflow.actions[actionkey].id); + firstnode = cy.getElementById(workflow.actions[actionkey].id); found = true; break; } @@ -2358,11 +2757,11 @@ const releaseToConnectLabel = "Release to Connect" return true; }; - const executeWorkflow = (executionArgument, startNode, hasSaved) => { + const executeWorkflow = (executionArgument, startNode, hasSaved, skip_popup) => { if (hasSaved === false) { setExecutionRequestStarted(true) - saveWorkflow(workflow, executionArgument, startNode); + saveWorkflow(workflow, executionArgument, startNode, undefined, skip_popup); //console.log("FIXME: Might have forgotten to save before executing."); return; } @@ -2384,67 +2783,75 @@ const releaseToConnectLabel = "Release to Connect" setExecutionRequest({}) stop() - // FIXME: Check if any node contains $exec in a param - // If they do, show a popup asking if they want to execute it without an execution argument, or to use a previous one - if (executionArgument === undefined || executionArgument === null || executionArgument.length === 0) + // FIXME: Check if any node contains $exec in a param + // If they do, show a popup asking if they want to execute it without an execution argument, or to use a previous one + if (skip_popup !== true && executionArgument === undefined || executionArgument === null || executionArgument.length === 0) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - var foundmissing = false - for (let actionkey in workflow.actions) { - if (workflow.actions[actionkey].parameters === undefined || workflow.actions[actionkey].parameters === null || workflow.actions[actionkey].parameters.length === 0) { - continue - } + if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { + var foundmissing = false + for (let actionkey in workflow.actions) { + if (workflow.actions[actionkey].parameters === undefined || workflow.actions[actionkey].parameters === null || workflow.actions[actionkey].parameters.length === 0) { + continue + } - for (let paramkey in workflow.actions[actionkey].parameters) { - const param = workflow.actions[actionkey].parameters[paramkey] - if (param.value === undefined || param.value === null || param.value.length === 0) { - continue - } + for (let paramkey in workflow.actions[actionkey].parameters) { + const param = workflow.actions[actionkey].parameters[paramkey] + if (param.value === undefined || param.value === null || param.value.length === 0) { + continue + } - if (param.value.indexOf("$exec") !== -1) { - foundmissing = true - break - } - } + if (param.value.indexOf("$exec") !== -1) { + foundmissing = true + break + } + } - if (foundmissing) { - break - } - } + if (foundmissing) { + break + } + } - if (foundmissing) { - //toast("This workflow contains a node that requires an execution argument. Please provide one.") - if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { - setExecutionRequestStarted(false) - setExecutionArgumentModalOpen(true) - return - } + if (foundmissing) { + //toast("This workflow contains a node that requires an execution argument. Please provide one.") + if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { + setExecutionRequestStarted(false) + setExecutionArgumentModalOpen(true) + return + } - if (workflowExecutions.length > 0) { - setExecutionRequestStarted(false) - setExecutionArgumentModalOpen(true) + if (workflowExecutions.length > 0) { + setExecutionRequestStarted(false) + setExecutionArgumentModalOpen(true) - return - } - } + return + } + } + } + + if (cy !== undefined && cy !== null) { + var curelements = cy.elements(); + for (let i = 0; i < curelements.length; i++) { + curelements[i].addClass("not-executing-highlight"); } - - var curelements = cy.elements(); - for (let i = 0; i < curelements.length; i++) { - curelements[i].addClass("not-executing-highlight"); } - var headers = { + var headers = { "Content-Type": "application/json", "Accept": "application/json", } - if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { - headers["Org-Id"] = workflow.org_id + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + if (workflow?.id === undefined || workflow?.id === null || workflow?.id?.length === 0) { + console.log("No workflow id found during execution") + workflow.id = props.match.params.key } const data = { execution_argument: executionArgument, start: startNode }; - fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`, + // fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`, + fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`, { method: "POST", headers: headers, @@ -2462,25 +2869,25 @@ const releaseToConnectLabel = "Release to Connect" }) .then((responseJson) => { if (!responseJson.success) { - //toast("Failed to start: " + responseJson.reason); - toast(responseJson.reason); - //toast.error(responseJson.reason); - setExecutionRunning(false); - setExecutionRequestStarted(false); - stop(); + //toast("Failed to start: " + responseJson.reason); + toast(responseJson.reason); + //toast.error(responseJson.reason); + setExecutionRunning(false); + setExecutionRequestStarted(false); + stop(); - for (let i = 0; i < curelements.length; i++) { - curelements[i].removeClass("not-executing-highlight"); - } - return; - } else { - if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) { - navigate(`?execution_id=${responseJson.execution_id}`) - } + for (let i = 0; i < curelements.length; i++) { + curelements[i].removeClass("not-executing-highlight"); + } + return; + } else { + if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) { + navigate(`?execution_id=${responseJson.execution_id}`) + } - setExecutionRunning(true); - setExecutionRequestStarted(false); - } + setExecutionRunning(true); + setExecutionRequestStarted(false); + } if ( responseJson.execution_id === "" || @@ -2494,11 +2901,11 @@ const releaseToConnectLabel = "Release to Connect" setExecutionRequestStarted(false); stop(); - for (let i = 0; i < curelements.length; i++) { - curelements[i].removeClass("not-executing-highlight"); - } - return; - } + for (let i = 0; i < curelements.length; i++) { + curelements[i].removeClass("not-executing-highlight"); + } + return; + } setExecutionRequest({ execution_id: responseJson.execution_id, @@ -2513,7 +2920,7 @@ const releaseToConnectLabel = "Release to Connect" //toast(error.toString()); setExecutionRequestStarted(false) console.log("Execute workflow err: ", error.toString()); - toast.warn("Failed to run the workflow. Is the network down?") + toast.warn("Failed to run the workflow. Is the network down?") }); }) }; @@ -2521,191 +2928,197 @@ const releaseToConnectLabel = "Release to Connect" // This can be used to only show prioritzed ones later // Right now, it can prioritize authenticated ones //"Testing", - // - // + // + // const getAuthGroups = (orgId) => { - setAuthGroups([]) - var headers = { - "content-type": "application/json", - "accept": "application/json", - } + setAuthGroups([]) + var headers = { + "content-type": "application/json", + "accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } fetch(globalUrl + "/api/v1/authentication/groups", { method: "GET", headers: headers, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for app auth :O!"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === true) { - setAuthGroups(responseJson.data) - } else { - console.log("AppAuth group loading error: " + responseJson.reason); - } - }) - .catch((error) => { - setAuthGroups([]); + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAuthGroups(responseJson.data) + } else { + console.log("AppAuth group loading error: " + responseJson.reason); + } + }) + .catch((error) => { + setAuthGroups([]); console.log("AppAuth group loading error: " + error.toString()); - }) + }) } const getAppAuthentication = (reset, updateAction, closeMenu, orgId) => { - var headers = { - "content-type": "application/json", - "accept": "application/json", - } + var headers = { + "content-type": "application/json", + "accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } fetch(globalUrl + "/api/v1/apps/authentication", { method: "GET", headers: headers, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for app auth :O!"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } - return response.json(); - }) - .then((responseJson) => { - var shouldClose = false - if (responseJson.success) { - getAuthGroups(orgId) + return response.json(); + }) + .then((responseJson) => { + var shouldClose = false + if (responseJson.success) { + getAuthGroups(orgId) - var newauth = []; - for (let authkey in responseJson.data) { - if (responseJson.data[authkey].defined === false) { - continue; - } + var newauth = []; + for (let authkey in responseJson.data) { + if (responseJson.data[authkey].defined === false) { + continue; + } - newauth.push(responseJson.data[authkey]); - } + newauth.push(responseJson.data[authkey]); + } - setAppAuthentication(newauth); + setAppAuthentication(newauth); - if (cy !== undefined) { - // Remove the old listener for select, run with new one - cy.removeListener("select"); + if (cy !== undefined && cy !== null) { + // Remove the old listener for select, run with new one + cy.removeListener("select"); - cy.on("select", "node", (e) => onNodeSelect(e, newauth)); - cy.on("select", "edge", (e) => onEdgeSelect(e)); - } + cy.on("select", "node", (e) => onNodeSelect(e, newauth)); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + } - if (updateAction === true) { - if (selectedApp.authentication.required) { - // Setup auth here :) - var appUpdates = false; - const authenticationOptions = []; + if (updateAction === true) { + if (selectedApp.authentication.required) { + // Setup auth here :) + var appUpdates = false; + const authenticationOptions = []; - var tmpAuth = JSON.parse(JSON.stringify(newauth)); - var latest = 0; - for (let authkey in tmpAuth) { - var item = tmpAuth[authkey]; + var tmpAuth = JSON.parse(JSON.stringify(newauth)); + var latest = 0; + for (let authkey in tmpAuth) { + var item = tmpAuth[authkey]; - //console.log("Got auth: ", item); + //console.log("Got auth: ", item); - const newfields = {}; - for (let filterkey in item.fields) { - newfields[item.fields[filterkey].key] = item.fields[filterkey].value; - } + const newfields = {}; + for (let filterkey in item.fields) { + newfields[item.fields[filterkey].key] = item.fields[filterkey].value; + } - item.fields = newfields; + item.fields = newfields; - const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) - const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) - if (itemname === appname) { - authenticationOptions.push(item); + const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) + const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) + if (itemname === appname) { + authenticationOptions.push(item); - // Always becoming the last one - if (item.edited > latest) { - latest = item.edited; - selectedAction.selectedAuthentication = item; + // Always becoming the last one + if (item.edited > latest) { + latest = item.edited; + selectedAction.selectedAuthentication = item; - for (let actionkey in workflow.actions) { - const actionAppname = workflow.actions[actionkey].app_name.toLowerCase().replaceAll(" ", "_", -1) - if (actionAppname === appname) { - workflow.actions[actionkey].selectedAuthentication = item; - workflow.actions[actionkey].authentication_id = item.id; - appUpdates = true; - } - } - } else { - //console.log("Not newer: ", item.edited, " vs ", latest) - } - } else { - //console.log("Appname is wrong: ", appname, " vs ", itemname) - } - } + for (let actionkey in workflow.actions) { + const actionAppname = workflow.actions[actionkey].app_name.toLowerCase().replaceAll(" ", "_", -1) + if (actionAppname === appname) { + workflow.actions[actionkey].selectedAuthentication = item; + workflow.actions[actionkey].authentication_id = item.id; + appUpdates = true; + } + } + } else { + //console.log("Not newer: ", item.edited, " vs ", latest) + } + } else { + //console.log("Appname is wrong: ", appname, " vs ", itemname) + } + } - console.log("auth options: ", authenticationOptions) + console.log("auth options: ", authenticationOptions) - selectedAction.authentication = authenticationOptions - if (selectedAction.selectedAuthentication === null || selectedAction.selectedAuthentication === undefined || selectedAction.selectedAuthentication.length === "") { - selectedAction.selectedAuthentication = {} - } + selectedAction.authentication = authenticationOptions + if (selectedAction.selectedAuthentication === null || selectedAction.selectedAuthentication === undefined || selectedAction.selectedAuthentication.length === "") { + selectedAction.selectedAuthentication = {} + } - if (appUpdates === true) { - console.log("Closing auth modal: Success") + if (appUpdates === true) { + console.log("Closing auth modal: Success") - setAuthenticationModalOpen(false); - setSelectedAction(selectedAction); - setWorkflow(workflow); - saveWorkflow(workflow); + setAuthenticationModalOpen(false); + setSelectedAction(selectedAction); + setWorkflow(workflow); + saveWorkflow(workflow); - toast("Added and updated authentication!"); - shouldClose = true - } else { - console.log("Closing auth modal? FAIL") + toast("Added and updated authentication!"); + shouldClose = true + } else { + console.log("Closing auth modal? FAIL") - toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); - shouldClose = false - } - } else { - toast("No authentication to update"); - } - } else { - shouldClose = true - } + toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); + shouldClose = false + } + } else { + toast("No authentication to update"); + } + } else { + shouldClose = true + } - } else { - setAppAuthentication([]) - shouldClose = true - } + } else { + setAppAuthentication([]) + shouldClose = true + } - // Auto-closing if changes were made - if (closeMenu === true && shouldClose === true) { - setAuthenticationModalOpen(false); - } - }) - .catch((error) => { - setAppAuthentication([]); - //toast("Auth loading error: " + error.toString()); - console.log("AppAuth error: " + error.toString()); - }); + // Auto-closing if changes were made + if (closeMenu === true && shouldClose === true) { + setAuthenticationModalOpen(false); + } + }) + .catch((error) => { + setAppAuthentication([]); + //toast("Auth loading error: " + error.toString()); + console.log("AppAuth error: " + error.toString()); + }); }; const getApps = () => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + fetch(globalUrl + "/api/v1/apps", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -2714,27 +3127,27 @@ const releaseToConnectLabel = "Release to Connect" const pretend_apps = [{ "name": "TBD", - "id": "TBD", + "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", "version": "TBD", "large_image": "", }] - - setAppsLoaded(true) + + setAppsLoaded(true) setFilteredApps(pretend_apps) setApps(Array.prototype.concat.apply(pretend_apps, triggers)) setPrioritizedApps(pretend_apps) - if (isLoggedIn) { - toast("Something went wrong while loading apps. Please refresh the window to try again.") - } + if (isLoggedIn) { + toast("Something went wrong while loading apps. Please refresh the window to try again.") + } return } - //console.log("Apps loaded. JSON decoding next") + //console.log("Apps loaded. JSON decoding next") return response.json() }) @@ -2743,15 +3156,15 @@ const releaseToConnectLabel = "Release to Connect" console.log("No response") const pretend_apps = [{ "name": "TBD", - "id": "TBD", + "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", "version": "TBD", "large_image": "", }] - - setAppsLoaded(true) + + setAppsLoaded(true) setFilteredApps(pretend_apps) setApps(Array.prototype.concat.apply(pretend_apps, triggers)) setPrioritizedApps(pretend_apps) @@ -2762,48 +3175,59 @@ const releaseToConnectLabel = "Release to Connect" return } - // Used for e.g. Liquid testing - const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") - if (foundTools !== undefined && foundTools !== null) { - setToolsApp(foundTools) + // Find app with ID "794e51c3c1a8b24b89ccc573a3defc47" (gmail) to force-break it, + // Find app with ID "3e2bdf9d5069fe3f4746c29d68785a6a" (shuffle tools) to force-break it, + // as to ensure the autocorrect works. + /* + const foundAppIndex = responseJson.findIndex((app) => app.id === "3e2bdf9d5069fe3f4746c29d68785a6a") + if (foundAppIndex !== -1) { + //responseJson[foundAppIndex].actions = responseJson[foundAppIndex].actions.slice(0, 1) + //console.log("Tools app: ", responseJson[foundAppIndex]) } + */ - // Set localstorage for the apps in the "apps" key + // Used for e.g. Liquid testing + const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") + if (foundTools !== undefined && foundTools !== null) { + setToolsApp(foundTools) + } + + // Set localstorage for the apps in the "apps" key setApps(Array.prototype.concat.apply(responseJson, triggers)) - if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { - try { - localStorage.setItem("apps", JSON.stringify(responseJson)) - } catch (e) { - console.log("Failed to set apps in localstorage: ", e) - } - } + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + try { + localStorage.setItem("apps", JSON.stringify(responseJson)) + } catch (e) { + console.log("Failed to set apps in localstorage: ", e) + } + } - var handledPrioritizedApps = responseJson.filter((app) => internalIds.includes(app.name.toLowerCase())); + var handledPrioritizedApps = responseJson.filter((app) => internalIds.includes(app.name.toLowerCase())); handledPrioritizedApps = [].concat(integrationApps, handledPrioritizedApps) if (isCloud) { setFilteredApps(responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase()))) setPrioritizedApps(handledPrioritizedApps) - + } else { const tmpFiltered = responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase())) setFilteredApps(tmpFiltered) setPrioritizedApps(handledPrioritizedApps) } - setAppsLoaded(true) + setAppsLoaded(true) - // Remove all cytoscape triggers first? - if (cy !== undefined && cy !== null) { - cy.removeListener("select") - } + // Remove all cytoscape triggers first? + if (cy !== undefined && cy !== null) { + cy.removeListener("select") + } - // Re-adding cytoscape triggers - if (cy !== undefined && cy !== null) { - cy.on("select", "node", (e) => { - onNodeSelect(e, appAuthentication) - }) - } + // Re-adding cytoscape triggers + if (cy !== undefined && cy !== null) { + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication) + }) + } }) .catch((error) => { console.log("App loading error: " + error.toString()) @@ -2851,14 +3275,14 @@ const releaseToConnectLabel = "Release to Connect" } const getFiles = (orgId) => { - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } fetch(globalUrl + "/api/v1/files", { method: "GET", @@ -2893,560 +3317,560 @@ const releaseToConnectLabel = "Release to Connect" }) .catch((error) => { //toast(error.toString()); - console.log("Error loading files: ", error) + console.log("Error loading files: ", error) }); - }; + }; - const onChunkedResponseComplete = (result) => { - // Dont return until in 5 seconds without setTimeout - } + const onChunkedResponseComplete = (result) => { + // Dont return until in 5 seconds without setTimeout + } - const onChunkedResponseError = (err) => { - if (streamDisabled) { - return - } - } + const onChunkedResponseError = (err) => { + if (streamDisabled) { + return + } + } - const uuidToHSV = (uuid) => { - // Convert the UUID to a hexadecimal string without dashes - const uuidHex = uuid.replace(/-/g, ""); - - // Take the first 6 characters of the hexadecimal UUID as the seed - const seed = parseInt(uuidHex.slice(0, 6), 16); - - // Normalize the seed to a value between 0 and 1 - const normalizedSeed = seed / 0xFFFFFF; // 0xFFFFFF is the maximum possible value with 6 hexadecimal characters - - // Use the normalized seed to generate HSV values - const hue = normalizedSeed; // Hue value between 0 and 1 - const saturation = 0.8; // You can adjust the saturation value as desired (between 0 and 1) - const value = 0.8; // You can adjust the value/brightness as desired (between 0 and 1) - - // Convert HSV to RGB - const rgb = HSVtoRGB(hue, saturation, value); - - // Scale the RGB values to the 0-255 range - const scaledRGB = rgb.map(val => Math.round(val * 255)); - - return scaledRGB; - } - - // HSV to RGB conversion function - const HSVtoRGB = (h, s, v) => { - const h_i = Math.floor(h * 6); - const f = h * 6 - h_i; - const p = v * (1 - s); - const q = v * (1 - f * s); - const t = v * (1 - (1 - f) * s); - - switch (h_i % 6) { - case 0: return [v, t, p]; - case 1: return [q, v, p]; - case 2: return [p, v, t]; - case 3: return [p, q, v]; - case 4: return [t, p, v]; - case 5: return [v, p, q]; - default: return [0, 0, 0]; - } - } + const uuidToHSV = (uuid) => { + // Convert the UUID to a hexadecimal string without dashes + const uuidHex = uuid.replace(/-/g, ""); - const rgbToHex = (rgb) => { - // Ensure that each component is in the valid range (0-255) - const r = Math.max(0, Math.min(255, rgb[0])); - const g = Math.max(0, Math.min(255, rgb[1])); - const b = Math.max(0, Math.min(255, rgb[2])); + // Take the first 6 characters of the hexadecimal UUID as the seed + const seed = parseInt(uuidHex.slice(0, 6), 16); - // Convert the RGB values to hexadecimal and pad with zeros if needed - const rHex = r.toString(16).padStart(2, "0"); - const gHex = g.toString(16).padStart(2, "0"); - const bHex = b.toString(16).padStart(2, "0"); + // Normalize the seed to a value between 0 and 1 + const normalizedSeed = seed / 0xFFFFFF; // 0xFFFFFF is the maximum possible value with 6 hexadecimal characters - // Combine the hexadecimal values to create the final color code - const hexColor = `#${rHex}${gHex}${bHex}`; + // Use the normalized seed to generate HSV values + const hue = normalizedSeed; // Hue value between 0 and 1 + const saturation = 0.8; // You can adjust the saturation value as desired (between 0 and 1) + const value = 0.8; // You can adjust the value/brightness as desired (between 0 and 1) - return hexColor.toUpperCase(); // Optionally, make the result uppercase - } + // Convert HSV to RGB + const rgb = HSVtoRGB(hue, saturation, value); - const getUserColor = (user_id) => { - //return "#ffffff" - return rgbToHex(uuidToHSV(user_id)) - } + // Scale the RGB values to the 0-255 range + const scaledRGB = rgb.map(val => Math.round(val * 255)); - const hoverNode = (chunkJson) => { - // Find the node - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + return scaledRGB; + } - var node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + // HSV to RGB conversion function + const HSVtoRGB = (h, s, v) => { + const h_i = Math.floor(h * 6); + const f = h * 6 - h_i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); - const color = getUserColor(chunkJson.user_id) - const parsedStyle = { - "border-width": "6px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + switch (h_i % 6) { + case 0: return [v, t, p]; + case 1: return [q, v, p]; + case 2: return [p, v, t]; + case 3: return [p, q, v]; + case 4: return [t, p, v]; + case 5: return [v, p, q]; + default: return [0, 0, 0]; + } + } - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) + const rgbToHex = (rgb) => { + // Ensure that each component is in the valid range (0-255) + const r = Math.max(0, Math.min(255, rgb[0])); + const g = Math.max(0, Math.min(255, rgb[1])); + const b = Math.max(0, Math.min(255, rgb[2])); - // Wait 3 seconds and remove it - setTimeout(() => { - const parsedStyle = { - "border-width": "1px", - "font-size": "18px", - "border-color": "#81c784", - } + // Convert the RGB values to hexadecimal and pad with zeros if needed + const rHex = r.toString(16).padStart(2, "0"); + const gHex = g.toString(16).padStart(2, "0"); + const bHex = b.toString(16).padStart(2, "0"); - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - }, 3000) - } + // Combine the hexadecimal values to create the final color code + const hexColor = `#${rHex}${gHex}${bHex}`; - const moveNode = (chunkJson) => { - // Find the node - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + return hexColor.toUpperCase(); // Optionally, make the result uppercase + } - var node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + const getUserColor = (user_id) => { + //return "#ffffff" + return rgbToHex(uuidToHSV(user_id)) + } - console.log("Moving node: ", node, chunkJson) + const hoverNode = (chunkJson) => { + // Find the node + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } - // Find nodes attached to the node - node.position({ - x: chunkJson.location.x, - y: chunkJson.location.y - }) + var node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } - const connectedNodes = cy.filter('node[attachedTo = "'+chunkJson.id+'"]') - if (connectedNodes === undefined || connectedNodes === null) { - console.log("Connected nodes is undefined or null") - } else { - console.log("Connected nodes: ", connectedNodes) - connectedNodes.remove() - } + const color = getUserColor(chunkJson.user_id) + const parsedStyle = { + "border-width": "6px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - const color = getUserColor(chunkJson.user_id) - const parsedStyle = { - "border-width": "11px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) + // Wait 3 seconds and remove it + setTimeout(() => { + const parsedStyle = { + "border-width": "1px", + "font-size": "18px", + "border-color": "#81c784", + } - // Wait 3 seconds and remove it - setTimeout(() => { - const parsedStyle = { - "border-width": "1px", - "font-size": "18px", - "border-color": "#81c784", - } + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + }, 3000) + } - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - }, 3000) - } + const moveNode = (chunkJson) => { + // Find the node + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } - const hoverEdge = (chunkJson) => { - // Find the node - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + var node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } - var node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + console.log("Moving node: ", node, chunkJson) - const color = getUserColor(chunkJson.user_id) - const parsedStyle = { - "line-gradient-stop-positions": ["0.0", "100"], - "line-gradient-stop-colors": [color, color], - } + // Find nodes attached to the node + node.position({ + x: chunkJson.location.x, + y: chunkJson.location.y + }) - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) + const connectedNodes = cy.filter('node[attachedTo = "' + chunkJson.id + '"]') + if (connectedNodes === undefined || connectedNodes === null) { + console.log("Connected nodes is undefined or null") + } else { + console.log("Connected nodes: ", connectedNodes) + connectedNodes.remove() + } - // Wait 3 seconds and remove it - setTimeout(() => { - const parsedStyle = { - "line-gradient-stop-positions": ["0.0", "100"], - "line-gradient-stop-colors": ["grey", "grey"], - } + const color = getUserColor(chunkJson.user_id) + const parsedStyle = { + "border-width": "11px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - }, 3000) - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) - const selectNode = (chunkJson) => { - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + // Wait 3 seconds and remove it + setTimeout(() => { + const parsedStyle = { + "border-width": "1px", + "font-size": "18px", + "border-color": "#81c784", + } - var node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + }, 3000) + } - const color = getUserColor(chunkJson.user_id) - const parsedStyle = { - "border-width": "11px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + const hoverEdge = (chunkJson) => { + // Find the node + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) + var node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } - setTimeout(() => { - const parsedStyle = { - "border-width": "11px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + const color = getUserColor(chunkJson.user_id) + const parsedStyle = { + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": [color, color], + } - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - }, 3000) - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) - const unselectNode = (chunkJson) => { - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + // Wait 3 seconds and remove it + setTimeout(() => { + const parsedStyle = { + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["grey", "grey"], + } - var node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + }, 3000) + } - const color = getUserColor(chunkJson.user_id) - const parsedStyle = { - "border-width": "1px", - "font-size": "18px", - "border-color": "#81c784", - } + const selectNode = (chunkJson) => { + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } + var node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } - const addNode = (chunkJson) => { - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + const color = getUserColor(chunkJson.user_id) + const parsedStyle = { + "border-width": "11px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - const node = cy.getElementById(chunkJson.id) - if (node !== undefined && node !== null) { - return - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) - const color = getUserColor(chunkJson.user_id) + setTimeout(() => { + const parsedStyle = { + "border-width": "11px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - // Create the node and add to cytoscape - const data = chunkJson.data - const nodeData = { - group: "nodes", - data: chunkJson.data, - position: { - x: data.x, - y: data.y - }, - } + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + }, 3000) + } - cy.add(nodeData) + const unselectNode = (chunkJson) => { + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } + + var node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } + + const color = getUserColor(chunkJson.user_id) + const parsedStyle = { + "border-width": "1px", + "font-size": "18px", + "border-color": "#81c784", + } + + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + + const addNode = (chunkJson) => { + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } + + const node = cy.getElementById(chunkJson.id) + if (node !== undefined && node !== null) { + return + } + + const color = getUserColor(chunkJson.user_id) + + // Create the node and add to cytoscape + const data = chunkJson.data + const nodeData = { + group: "nodes", + data: chunkJson.data, + position: { + x: data.x, + y: data.y + }, + } + + cy.add(nodeData) if (workflowAsCode) { setWorkflowAsCode(false) } - - // Wait 100ms then add a style for it - setTimeout(() => { - const node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null during auto add from other user") - return - } - const parsedStyle = { - "border-width": "11px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + // Wait 100ms then add a style for it + setTimeout(() => { + const node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null during auto add from other user") + return + } - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - }, 100) - } + const parsedStyle = { + "border-width": "11px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - const removeNodeStream = (chunkJson) => { - if (cy === undefined || cy === null) { - console.log("Cy is undefined or null") - return - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + }, 100) + } - const node = cy.getElementById(chunkJson.id) - if (node === undefined || node === null) { - console.log("Node is undefined or null") - return - } + const removeNodeStream = (chunkJson) => { + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } - const color = getUserColor(chunkJson.user_id) + const node = cy.getElementById(chunkJson.id) + if (node === undefined || node === null) { + console.log("Node is undefined or null") + return + } - // Animate node, then delete 1 sec later - const parsedStyle = { - "border-width": "11px", - "border-opacity": ".7", - "font-size": "25px", - "border-color": color, - } + const color = getUserColor(chunkJson.user_id) - const animationDuration = 150 - node.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) + // Animate node, then delete 1 sec later + const parsedStyle = { + "border-width": "11px", + "border-opacity": ".7", + "font-size": "25px", + "border-color": color, + } - setTimeout(() => { - node.remove() - }, 1000) - } + const animationDuration = 150 + node.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) - const processChunkedResponse = async (response) => { - console.log("In process resp!") + setTimeout(() => { + node.remove() + }, 1000) + } - var text = ''; - var reader = response.body.getReader() - var decoder = new TextDecoder(); - - const appendChunks = (result) => { - var chunk = decoder.decode(result.value || new Uint8Array, {stream: !result.done}); + const processChunkedResponse = async (response) => { + console.log("In process resp!") - if (chunk === undefined || chunk === null) { - console.log("Chunk is undefined or null") - } + var text = ''; + var reader = response.body.getReader() + var decoder = new TextDecoder(); - // Try chunk JSON loading - try { - var chunkJson = JSON.parse(chunk) + const appendChunks = (result) => { + var chunk = decoder.decode(result.value || new Uint8Array, { stream: !result.done }); - if (chunkJson.success === false) { - console.log("Chunk failed: ", chunkJson) + if (chunk === undefined || chunk === null) { + console.log("Chunk is undefined or null") + } - if (!streamDisabled) { - setStreamDisabled(true) - streamDisabled2 = true - } - return - } + // Try chunk JSON loading + try { + var chunkJson = JSON.parse(chunk) + + if (chunkJson.success === false) { + console.log("Chunk failed: ", chunkJson) + + if (!streamDisabled) { + setStreamDisabled(true) + streamDisabled2 = true + } + return + } - if (chunkJson.item !== undefined && chunkJson.item !== null && chunkJson.item !== "") { - if (chunkJson.item === "node") { - if (chunkJson.type === "move") { - moveNode(chunkJson) - } else if (chunkJson.type === "hover") { - hoverNode(chunkJson) - } else if (chunkJson.type === "select") { - selectNode(chunkJson) - } else if (chunkJson.type === "unselect") { - unselectNode(chunkJson) - } else if (chunkJson.type === "add") { - addNode(chunkJson) - } else if (chunkJson.type === "remove") { - removeNodeStream(chunkJson) - } - } else if (chunkJson.item === "edge") { - if (chunkJson.type === "hover") { - // Same as node function? - //hoverEdge(chunkJson) - } - } - } - } catch (e) { - console.log("Chunk JSON error: ", e) - - if (!streamDisabled) { - setStreamDisabled(true) - streamDisabled2 = true - } - - return - } + if (chunkJson.item !== undefined && chunkJson.item !== null && chunkJson.item !== "") { + if (chunkJson.item === "node") { + if (chunkJson.type === "move") { + moveNode(chunkJson) + } else if (chunkJson.type === "hover") { + hoverNode(chunkJson) + } else if (chunkJson.type === "select") { + selectNode(chunkJson) + } else if (chunkJson.type === "unselect") { + unselectNode(chunkJson) + } else if (chunkJson.type === "add") { + addNode(chunkJson) + } else if (chunkJson.type === "remove") { + removeNodeStream(chunkJson) + } + } else if (chunkJson.item === "edge") { + if (chunkJson.type === "hover") { + // Same as node function? + //hoverEdge(chunkJson) + } + } + } + } catch (e) { + console.log("Chunk JSON error: ", e) - //data.push(chunk) - //setData(data) - - //setUpdate(Math.random()); + if (!streamDisabled) { + setStreamDisabled(true) + streamDisabled2 = true + } - //console.log('got chunk of', chunk.length, 'bytes. Value: ', chunk) - text += chunk; - //console.log('text so far is', text.length, 'bytes - if (result.done) { - console.log('returning') - return text; - } else { - return readChunk() - } - } + return + } - const readChunk = () => { - return reader.read().then(appendChunks); - } + //data.push(chunk) + //setData(data) - return readChunk(); - } + //setUpdate(Math.random()); + + //console.log('got chunk of', chunk.length, 'bytes. Value: ', chunk) + text += chunk; + //console.log('text so far is', text.length, 'bytes + if (result.done) { + console.log('returning') + return text; + } else { + return readChunk() + } + } + + const readChunk = () => { + return reader.read().then(appendChunks); + } + + return readChunk(); + } async function fetchWithTimeout(resource, options = {}) { const { timeout = 8000 } = options; - + const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); - + const response = await fetch(resource, { - ...options, - signal: controller.signal + ...options, + signal: controller.signal }); clearTimeout(id); - + return response; } const startWorkflowStream = async (workflowId) => { - if (!isCloud) { - console.log("Not cloud, not starting workflow stream") - return - } + if (!isCloud) { + console.log("Not cloud, not starting workflow stream") + return + } - if (streamDisabled) { - console.log("Stream listener disabled") - return - } + if (streamDisabled) { + console.log("Stream listener disabled") + return + } - const timeout = 60000 + const timeout = 60000 //const url = `${globalUrl}/api/v1/workflows/${workflowId}/stream` - //const streamUrl = "https://shuffle-streaming-backend-stbuwivzoq-ew.a.run.app" - // - const streamUrl = "https://stream.shuffler.io" - const url = `${streamUrl}/api/v1/workflows/${workflowId}/stream` - while (true) { - if (streamDisabled === true || streamDisabled2 === true) { - console.log("Stream disabled, breaking") - break - } + //const streamUrl = "https://shuffle-streaming-backend-stbuwivzoq-ew.a.run.app" + // + const streamUrl = "https://stream.shuffler.io" + const url = `${streamUrl}/api/v1/workflows/${workflowId}/stream` + while (true) { + if (streamDisabled === true || streamDisabled2 === true) { + console.log("Stream disabled, breaking") + break + } - // Wait 1 second before next request just in case of timeouts - await new Promise(r => setTimeout(r, 1000)); - await fetchWithTimeout(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - timeout: timeout, - }) - .then(processChunkedResponse) - .then(onChunkedResponseComplete) - .catch(onChunkedResponseError) - } + // Wait 1 second before next request just in case of timeouts + await new Promise(r => setTimeout(r, 1000)); + await fetchWithTimeout(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + timeout: timeout, + }) + .then(processChunkedResponse) + .then(onChunkedResponseComplete) + .catch(onChunkedResponseError) + } } const [usedSubflowApps, setUsedSubflowApps] = React.useState([]); @@ -3478,7 +3902,7 @@ const releaseToConnectLabel = "Release to Connect" for (let index in responseJson.actions) { apps.push(responseJson.actions[index]); } - + console.log("Setting used subflow apps: ", apps) setUsedSubflowApps(apps); @@ -3489,45 +3913,197 @@ const releaseToConnectLabel = "Release to Connect" }); return apps - }; + } + + const findWorkflowDiff = (parentWorkflow, childWorkflow) => { + var diff = { + "different": false, + "environment": false, + "actions": [], + "triggers": [], + } + + if (parentWorkflow.actions === undefined || parentWorkflow.actions === null || parentWorkflow.actions.length === 0) { + console.log("Parent workflow actions are empty") + return diff + } + + if (childWorkflow.actions === undefined || childWorkflow.actions === null || childWorkflow.actions.length === 0) { + console.log("Child workflow actions are empty") + return diff + } + + + var parentEnvironment = "" + var childEnvironment = "" + for (var parentKey in parentWorkflow.actions) { + const parentAction = parentWorkflow.actions[parentKey] + if (parentAction.environment !== undefined && parentAction.environment !== null && parentAction.environment !== "") { + parentEnvironment = parentAction.environment + } + + var actionDiff = { + parameters: [] + } + + var found = false + for (var childKey in childWorkflow.actions) { + const childAction = childWorkflow.actions[childKey] + if (childAction.environment !== undefined && childAction.environment !== null && childAction.environment !== "") { + childEnvironment = childAction.environment + } + + if (childAction.id !== parentAction.id) { + found = true + continue + } + + if (childAction.label !== parentAction.label) { + actionDiff.label_change = true + } + + /* + if (childAction.app_id !== parentAction.app_id) { + actionDiff.app_id = true + } + */ + + if (childAction.app_name !== parentAction.app_name) { + actionDiff.app_name = true + } + + if (childAction.app_version !== parentAction.app_version) { + actionDiff.app_version = true + } + + if (childAction.name !== parentAction.name) { + actionDiff.name = true + } + + // Irrelevant + //if (childAction.environment !== parentAction.environment) { + // actionDiff.environment = true + //} + + if (childAction.authentication_id !== parentAction.authentication_id) { + actionDiff.authentication_id = true + } + + if (parentAction.parameters === undefined || parentAction.parameters === null || parentAction.parameters.length === 0 || childAction.parameters === undefined || childAction.parameters === null || childAction.parameters.length === 0) { + continue + } + + for (var parentParamIndex in parentAction.parameters) { + const parentParam = parentAction.parameters[parentParamIndex] + for (var childParamIndex in childAction.parameters) { + const childParam = childAction.parameters[childParamIndex] + if (childParam.name !== parentParam.name) { + continue + } + + if (childParam.value !== parentParam.value) { + actionDiff.parameters.push(childParam.name) + } + } + } + } + + if (actionDiff.parameters.length > 0) { + actionDiff.params = true + } + + if (!found) { + actionDiff.new = true + } + + if (actionDiff !== undefined && actionDiff !== null && Object.keys(actionDiff).length > 1) { + actionDiff.label = parentAction.label.replaceAll("_", " ") + actionDiff.id = parentAction.id + actionDiff.large_image = parentAction.large_image + diff.actions.push(actionDiff) + } + } + + if (childEnvironment !== parentEnvironment) { + diff.environment = true + } + + + // loop diff and find if ANY key is true + for (var key in diff) { + try { + if (diff[key] === true || diff[key].length > 0) { + diff.different = true + break + } + } catch (e) { + console.log("Error in diff: ", e) + } + } + + return diff + } const getChildWorkflows = (parentWorkflowId) => { - //toast("Loading child workflows 1 (should be 2)") + var originalChildWorkflows = [] + try { + originalChildWorkflows = JSON.parse(JSON.stringify(suborgWorkflows)) + } catch (e) { + console.log("Error in parsing suborg workflows: ", e) + } + setSuborgWorkflows([]) + //toast("Loading child workflows 1 (should be 2)") - /* - if (originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0) { - return - } - */ + /* + if (originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0) { + return + } + */ - const orgId = originalWorkflow.org_id === undefined || originalWorkflow.org_id === null || originalWorkflow.org_id === "" ? "" : originalWorkflow.org_id - - //toast("Loading child workflows 2: " + orgId) + const orgId = originalWorkflow.org_id === undefined || originalWorkflow.org_id === null || originalWorkflow.org_id === "" ? "" : originalWorkflow.org_id fetch(`${globalUrl}/api/v1/workflows/${parentWorkflowId}/child_workflows`, { method: "GET", headers: { "Content-Type": "application/json", "Accept": "application/json", - "Org-Id": orgId, + "Org-Id": orgId, }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflows :O!"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success !== false) { - setSuborgWorkflows(responseJson) - } - }) - .catch((error) => { - console.log("Get child workflows error: ", error); - }) + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + + // FIXME: There is a timing problem here somewhere. + for (var key in responseJson) { + const diff = findWorkflowDiff(originalWorkflow, responseJson[key]) + if (diff !== undefined && diff !== null) { + responseJson[key].diff = diff + } + } + + setTimeout(() => { + setSuborgWorkflows(responseJson) + }, 500) + } else { + setTimeout(() => { + setSuborgWorkflows(originalChildWorkflows) + }, 500) + } + }) + .catch((error) => { + setTimeout(() => { + setSuborgWorkflows(originalChildWorkflows) + }, 500) + console.log("Get child workflows error: ", error); + }) } const getWorkflow = (workflow_id, sourcenode) => { @@ -3541,53 +4117,62 @@ const releaseToConnectLabel = "Release to Connect" }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for workflows :O!"); + console.log("Status not 200 for workflows :O!"); - if (response.status >= 500) { - toast("Something went wrong while loading the workflow. Please reload.") - } else { + if (response.status >= 500) { + toast("Something went wrong while loading the workflow. Please reload.") + } else { - // Check for execution_id in URL - // don't redirect if it exists - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - var execFound = new URLSearchParams(cursearch).get("execution_id"); - var sessionToken = new URLSearchParams(cursearch).get("session_token"); - if (execFound === null && sessionToken === null) { - toast(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds..`) - setTimeout(() => { - window.location.pathname = "/workflows"; - }, 2000); + // Check for execution_id in URL + // don't redirect if it exists + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + var execFound = new URLSearchParams(cursearch).get("execution_id"); + var sessionToken = new URLSearchParams(cursearch).get("session_token"); + if (execFound === null && sessionToken === null) { - } else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { - toast(`Injecting session token and reloading workflow..`) - setTimeout(() => { - setCookie("session_token", sessionToken, { path: "/" }); - window.location.href = "https://shuffler.io/workflows/3abdfb21-b40f-4e50-b855-ac0d62f83cbe"; - }, 2000) - } + toast.error(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds. If you recently deleted this workflow, speak with support@shuffler.io to recover it from a revision.`, { + autoClose: 10000, + }) + + setTimeout(() => { + window.location.pathname = "/workflows"; + }, 2500); + + } else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { + toast(`Injecting session token and reloading workflow..`) + setTimeout(() => { + setCookie("session_token", sessionToken, { path: "/" }); + window.location.href = "https://shuffler.io/workflows/3abdfb21-b40f-4e50-b855-ac0d62f83cbe"; + }, 2000) + } else if (execFound !== null && response.status >= 300) { + toast.info("Failed to load the workflow, but you may still find a list of workflow runs if you have access.") + setExecutionModalOpen(true) } + } } - // Read text from stream - //return response.text(); + // Read text from stream + //return response.text(); return response.json(); }) .then((responseJson) => { - // Load as JSON - if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.id !== workflow_id) { - toast("Workflow ID mismatch. Redirecting to your workflow") - navigate(`/workflows/${responseJson.id}`) - } + // Load as JSON + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.id !== workflow_id) { + toast.warning("Workflow ID mismatch. Redirected to your actual workflow. This may happen due to being in the wrong org, where we load the child workflow for you automatically.", { + autoClose: 10000, + }) + navigate(`/workflows/${responseJson.id}`) + } - if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow !== "") { - setDistributedFromParent(responseJson.parentorg_workflow) - } + if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow !== "") { + setDistributedFromParent(responseJson.parentorg_workflow) + } - if (responseJson.childorg_workflow_ids !== undefined && responseJson.childorg_workflow_ids !== null && responseJson.childorg_workflow_ids.length > 0) { - getChildWorkflows(responseJson.id) - } else if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow.length > 0) { - getChildWorkflows(responseJson.parentorg_workflow) - } + if (responseJson.childorg_workflow_ids !== undefined && responseJson.childorg_workflow_ids !== null && responseJson.childorg_workflow_ids.length > 0) { + getChildWorkflows(responseJson.id) + } else if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow.length > 0) { + getChildWorkflows(responseJson.parentorg_workflow) + } // Not sure why this is necessary. if (responseJson.isValid === undefined) { @@ -3606,62 +4191,75 @@ const releaseToConnectLabel = "Release to Connect" responseJson.triggers = []; } - if (responseJson.org_id !== undefined && responseJson.org_id !== null) { - listOrgCache(responseJson.org_id) - } + if (responseJson.org_id !== undefined && responseJson.org_id !== null) { + listOrgCache(responseJson.org_id) + } - if (responseJson.sharing !== undefined && responseJson.sharing !== null && (responseJson.sharing === "form" || responseJson.sharing === "forms")) { - if (responseJson.actions === undefined || responseJson.actions === null || responseJson.actions.length === 0) { - navigate("/forms/" + responseJson.id) - toast("Redirecting to Form from Workflow") - } - } - - // Wait for this to finish - fetchRecommendations(responseJson) + if (responseJson.sharing !== undefined && responseJson.sharing !== null && (responseJson.sharing === "form" || responseJson.sharing === "forms")) { + if (responseJson.actions === undefined || responseJson.actions === null || responseJson.actions.length === 0) { + navigate("/forms/" + responseJson.id) + toast("Redirecting to Form from Workflow") + } + } + + // Wait for this to finish + fetchRecommendations(responseJson) if (responseJson.public) { - setAppAuthentication([]) - console.log("RESP: ", responseJson) + setAppAuthentication([]) + setLeftBarSize(300) + if (Object.getOwnPropertyNames(creatorProfile).length === 0) { //getUserProfile("frikky") getUserProfile(responseJson.id, false) } - //{appGroup.map((data, index) => { - //const [appGroup, setAppGroup] = React.useState([]); - var appsFound = [] - for (let actionkey in responseJson.actions) { - const parsedAction = responseJson.actions[actionkey] - if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") { - continue - } - if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ - appsFound.push(parsedAction) - } - } + //{appGroup.map((data, index) => { + //const [appGroup, setAppGroup] = React.useState([]); + var appsFound = [] + for (let actionkey in responseJson.actions) { + const parsedAction = responseJson.actions[actionkey] + if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") { + continue + } + if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0) { + appsFound.push(parsedAction) + } + } setAppGroup(appsFound) - appsFound = [] - for (let triggerkey in responseJson.triggers) { - const parsedAction = responseJson.triggers[triggerkey] - if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){ - appsFound.push(parsedAction) - } - } + appsFound = [] + for (let triggerkey in responseJson.triggers) { + const parsedAction = responseJson.triggers[triggerkey] + if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0) { + appsFound.push(parsedAction) + } + } setTriggerGroup(appsFound) - setWorkflows([responseJson]) + setWorkflows([responseJson]) + setCurrentWorkflow(responseJson) } else { - getAppAuthentication(); - getEnvironments(); + getAppAuthentication() + + var defaultEnvironmentName = undefined + if (responseJson.actions !== undefined && responseJson.actions !== null && responseJson.actions.length > 0) { + for (var i = 0; i < responseJson.actions.length; i++) { + const curaction = responseJson.actions[i] + if (curaction.environment !== undefined && curaction.environment !== null && curaction.environment.length > 0) { + defaultEnvironmentName = curaction.environment + break + } + } + } + + getEnvironments(responseJson.org_id, defaultEnvironmentName) - getSettings(); getFiles() - getWorkflowExecution(props.match.params.key, ""); - getAvailableWorkflows(-1); + getWorkflowExecution(responseJson.id, "") + getAvailableWorkflows(-1) } @@ -3670,14 +4268,14 @@ const releaseToConnectLabel = "Release to Connect" var nodefound = false; var target = sourcenode.parameters.find((item) => item.name === "startnode"); - console.log("Got rightclick target: ", target) - if (target === undefined || target === null) { - target = { - "name": "startnode", - "value": responseJson.start - } - } - + console.log("Got rightclick target: ", target) + if (target === undefined || target === null) { + target = { + "name": "startnode", + "value": responseJson.start + } + } + console.log(sourcenode.parameters); console.log(target); const target_id = target === undefined ? "" : target.value; @@ -3810,16 +4408,22 @@ const releaseToConnectLabel = "Release to Connect" decorator: true, source_workflow: responseJson.id, }, - }); + }) + } else { + console.log("Node not found: ", target_id) } - cy.fit(null, 400); + try { + cy.fit(null, 400); + } catch (e) { + console.log("Error in fitting (1): ", e) + } cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); } else { - if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.parentorg_workflow === "") { - setOriginalWorkflow(responseJson) - } + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.parentorg_workflow === "") { + setOriginalWorkflow(responseJson) + } setWorkflow(responseJson); setWorkflowDone(true); @@ -3834,10 +4438,10 @@ const releaseToConnectLabel = "Release to Connect" responseJson.errors !== // what responseJson.errors.length > 0 ) { - console.log("Setting configure Modal to open") + console.log("Setting configure Modal to open") } - - setConfigureWorkflowModalOpen(true) + + setConfigureWorkflowModalOpen(true) } } }) @@ -3870,19 +4474,19 @@ const releaseToConnectLabel = "Release to Connect" } } - // Ensuring overwriting - if (nodedata?.type === "ACTION") { + // Ensuring overwriting + if (nodedata?.type === "ACTION") { - if (nodedata?.parameters !== undefined && nodedata.parameters !== null && nodedata.parameters.length > 0 && workflow?.actions !== undefined && workflow?.actions !== null && workflow?.actions.length > 0) { - for (var actionkey in workflow.actions) { - const action = workflow.actions[actionkey] - if (action.id === nodedata.id) { - workflow.actions[actionkey].parameters = nodedata.parameters - break - } - } - } - } + if (nodedata?.parameters !== undefined && nodedata.parameters !== null && nodedata.parameters.length > 0 && workflow?.actions !== undefined && workflow?.actions !== null && workflow?.actions.length > 0) { + for (var actionkey in workflow.actions) { + const action = workflow.actions[actionkey] + if (action.id === nodedata.id) { + workflow.actions[actionkey].parameters = nodedata.parameters + break + } + } + } + } // Unselecting all //cy.elements().unselect() @@ -3940,19 +4544,19 @@ const releaseToConnectLabel = "Release to Connect" //console.log("ACTION: ", selectedAction) //console.log("APP: ", selectedApp) - //setSubworkflow({}) + //setSubworkflow({}) ReactDOM.unstable_batchedUpdates(() => { setSelectedAction({}); setSelectedApp({}); setSelectedComment({}) setSelectedEdge({}) - //setSelectedActionEnvironment({}) setTriggerAuthentication({}) setLocalFirstrequest(true) setSelectedTrigger({}); setSelectedTriggerIndex(-1) - setUpdate(Math.random()) + setSubworkflow({}) + setUpdate(Math.random()) // Can be used for right side view setRightSideBarOpen(false); @@ -4025,7 +4629,7 @@ const releaseToConnectLabel = "Release to Connect" const nodedata = event.target.data(); console.log(nodedata); if (nodedata.type === "TRIGGER" && (nodedata.app_name === "Shuffle Workflow" || nodedata.app_name === "User Input")) { - + if (nodedata.parameters === null) { toast("Set a workflow first"); return; @@ -4047,13 +4651,18 @@ const releaseToConnectLabel = "Release to Connect" eles: event.target, }, }) - .play() - .promise() - .then(() => { - console.log("DONE: ", workflow_id); - getWorkflow(workflow_id.value, nodedata); - cy.fit(null, 300); - }); + .play() + .promise() + .then(() => { + console.log("DONE: ", workflow_id); + getWorkflow(workflow_id.value, nodedata); + + try { + cy.fit(null, 300); + } catch (e) { + console.log("Error in fitting (2): ", e) + } + }); } }; @@ -4068,34 +4677,34 @@ const releaseToConnectLabel = "Release to Connect" return } - const connected = event.target.connectedEdges().jsons() + const connected = event.target.connectedEdges().jsons() if (connected.length > 0 && connected !== undefined) { - for (let connectkey in connected) { - const edge = connected[connectkey] - if (edge.data.decorator && edge.data.label === releaseToConnectLabel) { - // Transform to normal edge - const currentedge = cy.getElementById(edge.data.id) - if (currentedge !== undefined && currentedge !== null) { - currentedge.data("decorator", false) - currentedge.data("label", "") - } - continue - } + for (let connectkey in connected) { + const edge = connected[connectkey] + if (edge.data.decorator && edge.data.label === releaseToConnectLabel) { + // Transform to normal edge + const currentedge = cy.getElementById(edge.data.id) + if (currentedge !== undefined && currentedge !== null) { + currentedge.data("decorator", false) + currentedge.data("label", "") + } + continue + } - const sourcenode = cy.getElementById(edge.data.source) - const destinationnode = cy.getElementById(edge.data.target) - if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { - continue - } + const sourcenode = cy.getElementById(edge.data.source) + const destinationnode = cy.getElementById(edge.data.target) + if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { + continue + } - const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) - const currentedge = cy.getElementById(edge.data.id) - if (currentedge !== undefined && currentedge !== null) { - currentedge.style('control-point-distance', edgeCurve.distance) - currentedge.style('control-point-weight', edgeCurve.weight) - } - } - } + const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) + const currentedge = cy.getElementById(edge.data.id) + if (currentedge !== undefined && currentedge !== null) { + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) + } + } + } if (styledElements.length === 1) { console.log( @@ -4168,73 +4777,73 @@ const releaseToConnectLabel = "Release to Connect" nodedata.app_name !== "User Input") || nodedata.isStartNode) ) { - const allNodes = cy.nodes().jsons(); - var found = false; - for (let nodekey in allNodes) { - const currentNode = allNodes[nodekey]; - if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) { - found = true - break - } - } + const allNodes = cy.nodes().jsons(); + var found = false; + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) { + found = true + break + } + } - if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - if (!found) { - //console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) - // Find how many executions it has - var executions = 0 - const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) - const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" - const decoratorNode = { - position: { - x: event.target.position().x + 44, - y: event.target.position().y + 44, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - isTrigger: true, - label: `${matchingExecutions.length}`, - attachedTo: nodedata.id, - imageColor: color, - hasExecutions: true, - }, - }; + if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { + if (!found) { + //console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) + // Find how many executions it has + var executions = 0 + const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) + const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" + const decoratorNode = { + position: { + x: event.target.position().x + 44, + y: event.target.position().y + 44, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + isTrigger: true, + label: `${matchingExecutions.length}`, + attachedTo: nodedata.id, + imageColor: color, + hasExecutions: true, + }, + }; - cy.add(decoratorNode) - } - } else { - // Readding the icon after moving the node - if (!found) { - const iconInfo = GetIconInfo(nodedata); - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + cy.add(decoratorNode) + } + } else { + // Readding the icon after moving the node + if (!found) { + const iconInfo = GetIconInfo(nodedata); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - const offset = nodedata.isStartNode ? 36 : 44; - const decoratorNode = { - position: { - x: event.target.position().x + offset, - y: event.target.position().y + offset, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - label: "", - image: svgpin_Url, - imageColor: iconInfo.iconBackgroundColor, - attachedTo: nodedata.id, - }, - }; + const offset = nodedata.isStartNode ? 36 : 44; + const decoratorNode = { + position: { + x: event.target.position().x + offset, + y: event.target.position().y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: nodedata.id, + }, + }; - cy.add(decoratorNode).unselectify(); - } else { - //console.log("Node already exists - don't add descriptor node"); - } - } + cy.add(decoratorNode).unselectify(); + } else { + //console.log("Node already exists - don't add descriptor node"); + } + } } originalLocation = { @@ -4253,6 +4862,177 @@ const releaseToConnectLabel = "Release to Connect" }) }; + // Check if it already has any non-decorator branches attached to it + const findClosestNode = (event, nodedata) => { + if (cy === undefined || cy === null) { + console.log("Cy is undefined or null") + return + } + + if (event === undefined || event === null) { + console.log("Event is undefined or null") + return + } + + if (event.target === undefined || event.target === null) { + console.log("Event target is undefined or null") + return + } + + if (!((nodedata?.trigger_type === "SUBFLOW" || nodedata?.trigger_type === "USERINPUT" || nodedata?.type === "ACTION") && !nodedata?.isStartNode)) { + //console.log("Not a valid node to find closest node for") + + return + } + + if (nodedata.finished === false) { + //console.log("Node is not finished") + return + } + + const branches = cy.elements('edge').jsons() + var branchFound = false + var decoratorNodeIds = [] + var decoratorIds = [] + for (var branchkey in branches) { + if (branches[branchkey].data.source === nodedata.id || branches[branchkey].data.target === nodedata.id) { + decoratorIds.push(branches[branchkey].data.id) + + if (branches[branchkey].data.decorator === true) { + + // Add the source/destination + if (branches[branchkey].data.source === nodedata.id) { + decoratorNodeIds.push(branches[branchkey].data.target) + } else { + decoratorNodeIds.push(branches[branchkey].data.source) + } + + continue + } + + //branchFound = true + //break + } + } + + if (!branchFound) { + var relevantNodes = [] + + const minDistance = 185 + const draggedNode = event.target + const allnodes = cy.nodes().jsons() + for (var nodekey in allnodes) { + const node = allnodes[nodekey] + if (node.data.id === nodedata.id) { + continue + } + + // Decorators + if (node.data.attachedTo !== undefined) { + continue + } + + if (node.position === undefined || node.position === null || node.position.x === undefined || node.position.y === undefined) { + continue + } + + if (node.data.type !== "ACTION" && node.data.type !== "TRIGGER") { + continue + } + + const distance = Math.sqrt( + Math.pow(draggedNode.position('x') - node.position.x, 2) + + Math.pow(draggedNode.position('y') - node.position.y, 2) + ) + + if (decoratorNodeIds.includes(node.data.id)) { + + // Drag a little farther to remove it + if (distance > minDistance + 75) { + // Remove the branch? Why? + const edgeToRemove = cy.getElementById(branches[branchkey].data.id) + if (edgeToRemove !== null && edgeToRemove !== undefined) { + //console.log("Removing edge: ", edgeToRemove) + edgeToRemove.remove() + break + } + } + } + + + if (distance < minDistance) { + relevantNodes.push(node) + //minDistance = distance + //closestNode = node + } + } + + for (var key in relevantNodes) { + const closestNode = relevantNodes[key] + if (closestNode.data.app_name === "Webhook" || closestNode.data.app_name === "Schedule") { + return + } + + // Checks if the branch already exists between the nodes + if (decoratorIds.length > 0) { + var foundBranch = false + for (var decoratorkey in decoratorIds) { + const decoratorEdge = cy.getElementById(decoratorIds[decoratorkey]) + if (decoratorEdge === null || decoratorEdge === undefined) { + continue + } + + // Check if source and destination exists with a branch + const sourceId = decoratorEdge.data("source") + const targetId = decoratorEdge.data("target") + if ((sourceId === closestNode.data.id && targetId === nodedata.id) || (sourceId === nodedata.id && targetId === closestNode.data.id)) { + foundBranch = true + break + } + } + + if (foundBranch) { + continue + } + } + + const newId = uuidv4() + cy.add({ + group: "edges", + data: { + decorator: true, + id: newId, + _id: newId, + source: closestNode.data.id, + target: nodedata.id, + label: releaseToConnectLabel, + conditions: [], + } + }) + } + } + + /* + // FIXME: This is the start of a highlighter for the node + // to better match it up with other elements + // 1. Get current node's position in X/Y on the screen + // 2. Draw a red line on the X and Y axis for positioning + + // Draw a red div line in the HTML + const position = event.target.position() + const redline = document.getElementById("redline") + if (redline !== null && redline !== undefined) { + redline.style.display = "block" + redline.style.position = "absolute" + redline.style.left = position.x + "px" + redline.style.top = position.y + "px" + redline.style.height = "10000px" + redline.style.width = 1 + console.log("REDLINE!") + } + */ + } + const onNodeDrag = (event, selectedAction) => { const nodedata = event.target.data(); @@ -4296,159 +5076,11 @@ const releaseToConnectLabel = "Release to Connect" return; } + // Finds closest partner to show edge to connect to if ((nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT" || nodedata.type === "ACTION") && !nodedata.isStartNode) { - // Check if it already has any non-decorator branches attached to it - const branches = cy.elements('edge').jsons() - var branchFound = false - var decoratorIds = [] - for (var branchkey in branches) { - if (branches[branchkey].data.source === nodedata.id || branches[branchkey].data.target === nodedata.id) { - - if (branches[branchkey].data.decorator === true) { - - // Add the source/destination - if (branches[branchkey].data.source === nodedata.id) { - decoratorIds.push(branches[branchkey].data.target) - } else { - decoratorIds.push(branches[branchkey].data.source) - } - - continue - } - - branchFound = true - break - } - } - - if (!branchFound) { - //console.log("Found action during drag. Checking closest nodes as it doesn't have a valid branch") - var closestNode = null - var minDistance = 300 - - const draggedNode = event.target - const allnodes = cy.nodes().jsons() - for (var nodekey in allnodes) { - const node = allnodes[nodekey] - if (node.data.id === nodedata.id) { - continue - } - - // Decorators - if (node.data.attachedTo !== undefined) { - continue - } - - if (node.position === undefined || node.position === null || node.position.x === undefined || node.position.y === undefined) { - continue - } - - if (node.data.type !== "ACTION" && node.data.type !== "TRIGGER") { - continue - } - - const distance = Math.sqrt( - Math.pow(draggedNode.position('x') - node.position.x, 2) + - Math.pow(draggedNode.position('y') - node.position.y, 2) - ) - - if (decoratorIds.includes(node.data.id)) { - //console.log("Found existing decorator for: ", node.data.app_name, "Distance: ", distance) - - if (distance > 300) { - // Remove the branch - const edgeToRemove = cy.getElementById(branches[branchkey].data.id) - if (edgeToRemove !== null && edgeToRemove !== undefined) { - //console.log("Removing edge: ", edgeToRemove) - edgeToRemove.remove() - //decoratorIds.splice(decoratorIds.indexOf(node.data.id), 1) - break - } - } - } - - - if (distance < minDistance) { - minDistance = distance - closestNode = node - } - } - - if (closestNode !== null && closestNode !== undefined) { - //console.log("Closest node app: ", closestNode.data.app_name, "Distance: ", minDistance) - - /* - if (decoratorIds.length > 0) { - console.log("Decorators already exists. If within distance of 15 add to existing, otherwise remove old and add new: ", decoratorIds) - for (var decoratorkey in decoratorIds) { - const decoratorEdge = cy.getElementById(decoratorIds[decoratorkey]) - if (decoratorEdge === null || decoratorEdge === undefined) { - continue - } - - const sourceNode = cy.getElementById(decoratorEdge.data.source) - const targetNode = cy.getElementById(decoratorEdge.data.target) - - const distance = Math.sqrt( - Math.pow(draggedNode.position('x') - sourceNode.position('x'), 2) + - Math.pow(draggedNode.position('y') - sourceNode.position('y'), 2) - ) - - // Check plus minus 15 in distance from mindistance - if (distance > minDistance - 15 && distance < minDistance + 15) { - console.log("Within distance of 15, add to existing edge") - } else { - console.log("Outside distance of 15, remove old edge and add new") - } - - } - } - */ - - if (decoratorIds.length === 0) { - //const edgeCurve = calculateEdgeCurve(draggedNode.position(), closestNode.position) - //currentedge.style('control-point-distance', edgeCurve.distance) - //currentedge.style('control-point-weight', edgeCurve.weight) - - const newId = uuidv4() - cy.add({ - group: "edges", - data: { - decorator: true, - id: newId, - _id: newId, - source: closestNode.data.id, - target: nodedata.id, - label: releaseToConnectLabel, - conditions: [], - } - }) - } - } - } - - /* - // FIXME: This is the start of a highlighter for the node - // to better match it up with other elements - // 1. Get current node's position in X/Y on the screen - // 2. Draw a red line on the X and Y axis for positioning - - // Draw a red div line in the HTML - const position = event.target.position() - const redline = document.getElementById("redline") - if (redline !== null && redline !== undefined) { - redline.style.display = "block" - redline.style.position = "absolute" - redline.style.left = position.x + "px" - redline.style.top = position.y + "px" - redline.style.height = "10000px" - redline.style.width = 1 - console.log("REDLINE!") - } - */ - + findClosestNode(event, nodedata) } - + if (originalLocation.x === 0 && originalLocation.y === 0 && nodedata.position !== undefined) { originalLocation.x = nodedata.position.x; originalLocation.y = nodedata.position.y; @@ -4521,330 +5153,329 @@ const releaseToConnectLabel = "Release to Connect" } }); - // Should get AI autocompletes - const aiSubmit = (value, setResponseMsg, setSuggestionLoading, inputAction) => { - if (setResponseMsg !== undefined) { - setResponseMsg("") - } + // Should get AI autocompletes + const aiSubmit = (value, setResponseMsg, setSuggestionLoading, inputAction) => { + if (setResponseMsg !== undefined) { + setResponseMsg("") + } - if (value === undefined || value === "") { - console.log("No value input!") - return - } + if (value === undefined || value === "") { + console.log("No value input!") + return + } - if (setSuggestionLoading !== undefined) { - setSuggestionLoading(true) - } - - console.log("Submit conversation with value: ", value); + if (setSuggestionLoading !== undefined) { + setSuggestionLoading(true) + } - // This is to find sample response and parse it as string - - var AppContext = [] - var originalParams = [] - var originalField = "" - - if (inputAction !== undefined && inputAction !== null) { - // Reload the data without copying - inputAction = JSON.parse(JSON.stringify(inputAction)) - originalParams = JSON.parse(JSON.stringify(inputAction.parameters)) - if (originalParams.length > 0) { - originalField = originalParams[0].name - } - const parents = getParents(inputAction) + console.log("Submit conversation with value: ", value); - var actionlist = [] - if (parents.length > 1) { - for (let [key,keyval] in Object.entries(parents)) { - const item = parents[key]; - if (item.label === "Execution Argument") { - continue; - } + // This is to find sample response and parse it as string - var exampledata = item.example === undefined || item.example === null ? "" : item.example; - // Find previous execution and their variables - //exampledata === "" && - if (workflowExecutions.length > 0) { - // Look for the ID - const found = false; - for (let [key,keyval] in Object.entries(workflowExecutions)) { - if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { - continue; - } + var AppContext = [] + var originalParams = [] + var originalField = "" - var foundResult = workflowExecutions[key].results.find((result) => result.action.id === item.id); - if (foundResult === undefined || foundResult === null) { - continue; - } + if (inputAction !== undefined && inputAction !== null) { + // Reload the data without copying + inputAction = JSON.parse(JSON.stringify(inputAction)) + originalParams = JSON.parse(JSON.stringify(inputAction.parameters)) + if (originalParams.length > 0) { + originalField = originalParams[0].name + } + const parents = getParents(inputAction) - if (foundResult.result !== undefined && foundResult.result !== null) { - foundResult = foundResult.result - } + var actionlist = [] + if (parents.length > 1) { + for (let [key, keyval] in Object.entries(parents)) { + const item = parents[key]; + if (item.label === "Runtime Argument") { + continue; + } - const valid = validateJson(foundResult, true) - if (valid.valid) { - if (valid.result.success === false) { - //console.log("Skipping success false autocomplete") - } else { - exampledata = valid.result; - break; - } - } else { - exampledata = foundResult; - } - } - } + var exampledata = item.example === undefined || item.example === null ? "" : item.example; + // Find previous execution and their variables + //exampledata === "" && + if (workflowExecutions.length > 0) { + // Look for the ID + const found = false; + for (let [key, keyval] in Object.entries(workflowExecutions)) { + if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { + continue; + } - // 1. Take - const itemlabelComplete = item.label === null || item.label === undefined ? "" : item.label.split(" ").join("_"); + var foundResult = workflowExecutions[key].results.find((result) => result.action.id === item.id); + if (foundResult === undefined || foundResult === null) { + continue; + } - const actionvalue = { - app_name: item.app_name, - action_name: item.name, - label: item.label, + if (foundResult.result !== undefined && foundResult.result !== null) { + foundResult = foundResult.result + } - type: "action", - id: item.id, - name: item.label, - autocomplete: itemlabelComplete, - example: exampledata, - }; + const valid = validateJson(foundResult, true) + if (valid.valid) { + if (valid.result.success === false) { + //console.log("Skipping success false autocomplete") + } else { + exampledata = valid.result; + break; + } + } else { + exampledata = foundResult; + } + } + } - actionlist.push(actionvalue); - } - } + // 1. Take + const itemlabelComplete = item.label === null || item.label === undefined ? "" : item.label.split(" ").join("_"); - var fixedResults = [] - for (var i = 0; i < actionlist.length; i++) { - const item = actionlist[i]; - const responseFix = SetJsonDotnotation(item.example, "") - - // Check if json - const validated = validateJson(responseFix) - var exampledata = responseFix; - if (validated.valid) { - exampledata = JSON.stringify(validated.result) - } + const actionvalue = { + app_name: item.app_name, + action_name: item.name, + label: item.label, - AppContext.push({ - "app_name": item.app_name, - "action_name": item.action_name, - "label": item.label, - "example": exampledata, - //"example_response": exampledata, - }) - } + type: "action", + id: item.id, + name: item.label, + autocomplete: itemlabelComplete, + example: exampledata, + } - var params = [] - for (var paramkey in inputAction.parameters) { - const param = inputAction.parameters[paramkey] - if (param.configuration) { - continue - } + console.log("VALUE: ", actionvalue) - // Mainly for booleans - if (param.options !== undefined && param.options !== null && param.options.length > 0) { - continue - } + actionlist.push(actionvalue); + } + } - params.push(param) - } + var fixedResults = [] + for (var i = 0; i < actionlist.length; i++) { + const item = actionlist[i]; + const responseFix = SetJsonDotnotation(item.example, "") - inputAction.parameters = params - } + // Check if json + const validated = validateJson(responseFix) + var exampledata = responseFix; + if (validated.valid) { + exampledata = JSON.stringify(validated.result) + } - var conversationData = { - "query": value, - "output_format": "action", - "app_context": AppContext, + AppContext.push({ + "app_name": item.app_name, + "action_name": item.action_name, + "label": item.label, + "example": exampledata, + //"example_response": exampledata, + }) + } - "workflow_id": workflow.id, - } + var params = [] + for (var paramkey in inputAction.parameters) { + const param = inputAction.parameters[paramkey] + if (param.configuration) { + continue + } + + // Mainly for booleans + if (param.options !== undefined && param.options !== null && param.options.length > 0) { + continue + } + + params.push(param) + } + + inputAction.parameters = params + } + + var conversationData = { + "query": value, + "output_format": "action", + "app_context": AppContext, + + "workflow_id": workflow.id, + } - if (inputAction !== undefined) { - console.log("Add app context! This should them get parameters directly") - conversationData.output_format = "action_parameters" + if (inputAction !== undefined) { + console.log("Add app context! This should them get parameters directly") + conversationData.output_format = "action_parameters" - conversationData.app_id = inputAction.app_id - conversationData.app_name = inputAction.app_name - conversationData.action_name = inputAction.name - conversationData.parameters = inputAction.parameters - } + conversationData.app_id = inputAction.app_id + conversationData.app_name = inputAction.app_name + conversationData.action_name = inputAction.name + conversationData.parameters = inputAction.parameters + } - // Onprem not available yet (April 2023) - // Should: Make OpenAI work for them with their own key - //fetch(`${globalUrl}/api/v1/conversation`, { - const url = `${globalUrl}/api/v1/conversation` - fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(conversationData), - credentials: "include", - }) - .then((response) => { - setAutocompleting(false) - if (setSuggestionLoading !== undefined) { - setSuggestionLoading(false) - } + // Onprem not available yet (April 2023) + // Should: Make OpenAI work for them with their own key + //fetch(`${globalUrl}/api/v1/conversation`, { + const url = `${globalUrl}/api/v1/conversation` + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(conversationData), + credentials: "include", + }) + .then((response) => { + setAutocompleting(false) + if (setSuggestionLoading !== undefined) { + setSuggestionLoading(false) + } - if (response.status !== 200) { - console.log("Status not 200 for stream results :O!"); - } else { - toast("Completion finished. Please verify the output and run the workflow again!") - } + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } else { + toast("Completion finished. Please verify the output and run the workflow again!") + } - return response.json(); - }) - .then((responseJson) => { - console.log("Conversation response: ", responseJson) - if (responseJson.success === false) { - if (responseJson.reason !== undefined) { - if (setResponseMsg !== undefined) { - setResponseMsg(responseJson.reason) - } + return response.json(); + }) + .then((responseJson) => { + console.log("Conversation response: ", responseJson) + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + if (setResponseMsg !== undefined) { + setResponseMsg(responseJson.reason) + } - toast.error(responseJson.reason) - } + toast.error(responseJson.reason) + } - return - } else { - setAiQueryModalOpen(false) - } + return + } else { + setAiQueryModalOpen(false) + } - if (inputAction !== undefined) { - console.log("In input action! Should check params if they match, and add suggestions") + if (inputAction !== undefined) { + console.log("In input action! Should check params if they match, and add suggestions") - console.log("ORIGINAL PARAMS: ", originalParams) - console.log("RESPONSE PARAMS: ", responseJson.parameters) + if (responseJson.parameters === undefined || responseJson.parameters.length === 0) { + return + } - if (responseJson.parameters === undefined || responseJson.parameters.length === 0) { - return - } + var changed = false + var codeeditorfound = false + for (let respParamKey in responseJson.parameters) { + var respParam = responseJson.parameters[respParamKey] + if (respParam.value === undefined || respParam.value === null || respParam.value === "") { + continue + } - var changed = false - var codeeditorfound = false - for (let respParamKey in responseJson.parameters) { - var respParam = responseJson.parameters[respParamKey] - if (respParam.value === undefined || respParam.value === null || respParam.value === "" ) { - continue - } + for (var paramkey in selectedAction?.parameters) { + const actionParam = selectedAction.parameters[paramkey] + if (actionParam.name !== respParam.name) { + continue + } - for (var paramkey in selectedAction?.parameters) { - const actionParam = selectedAction.parameters[paramkey] - if (actionParam.name !== respParam.name) { - continue - } + const codeeditor = document.getElementById("shuffle-codeeditor") + if (codeeditor !== undefined && codeeditor !== null && actionParam.name === originalField) { + const editorInstance = window?.ace?.edit("shuffle-codeeditor") + if (editorInstance === undefined || editorInstance === null) { + toast.error("Failed to find code editor instance") + return + } else { + codeeditorfound = true + editorInstance.setValue(respParam.value) + //selectedAction.parameters[paramkey].value = respParam.value + changed = true + } + } - const codeeditor = document.getElementById("shuffle-codeeditor") - if (codeeditor !== undefined && codeeditor !== null && actionParam.name === originalField) { - const editorInstance = window?.ace?.edit("shuffle-codeeditor") - if (editorInstance === undefined || editorInstance === null) { - toast.error("Failed to find code editor instance") - return - } else { - codeeditorfound = true - editorInstance.setValue(respParam.value) - //selectedAction.parameters[paramkey].value = respParam.value - changed = true - } - } + if (!changed) { + console.log("Found match for param: ", respParam) + changed = true + selectedAction.parameters[paramkey].autocompleted = true + selectedAction.parameters[paramkey].value = respParam.value + } + } + } - if (!changed) { - console.log("Found match for param: ", respParam) - changed = true - selectedAction.parameters[paramkey].autocompleted = true - selectedAction.parameters[paramkey].value = respParam.value - } - } - } + if (changed === true && codeeditorfound === false) { + //inputAction.parameters = JSON.parse(JSON.stringify(selectedAction.parameters)) + selectedAction.parameters = JSON.parse(JSON.stringify(selectedAction.parameters)) + console.log("Setting action! Force update pls :)") + setUpdate(Math.random()) - if (changed === true && codeeditorfound === false) { - //inputAction.parameters = JSON.parse(JSON.stringify(selectedAction.parameters)) - selectedAction.parameters = JSON.parse(JSON.stringify(selectedAction.parameters)) - console.log("Setting action! Force update pls :)") - setUpdate(Math.random()) + setSelectedAction(selectedAction) - setSelectedAction(selectedAction) + // Find it in cytoscape and update the action + if (cy !== undefined && cy !== null) { + const cyAction = cy.getElementById(inputAction.id) + if (cyAction !== undefined && cyAction !== null) { + cyAction.data("parameters", selectedAction.parameters) + } + } - // Find it in cytoscape and update the action - if (cy !== undefined && cy !== null) { - const cyAction = cy.getElementById(inputAction.id) - if (cyAction !== undefined && cyAction !== null) { - cyAction.data("parameters", selectedAction.parameters) - } - } + } - } + return + } - return - } + // Add action + console.log("Suggestionbox location: ", suggestionBox) + if (responseJson.app_name !== undefined && responseJson.app_name !== null) { + // Always added to 0, 0 + // Should use suggestionBox.position.x, suggestionBox.position.y + var newitem = { + "data": responseJson, + "position": { + "x": suggestionBox.node_position.x !== undefined ? suggestionBox.node_position.x : 0, + "y": suggestionBox.node_position.y !== undefined ? suggestionBox.node_position.y + 100 : 0, + }, + "group": "nodes", + } - // Add action - console.log("Suggestionbox location: ", suggestionBox) - if (responseJson.app_name !== undefined && responseJson.app_name !== null) { - // Always added to 0, 0 - // Should use suggestionBox.position.x, suggestionBox.position.y - var newitem = { - "data": responseJson, - "position": { - "x": suggestionBox.node_position.x !== undefined ? suggestionBox.node_position.x : 0, - "y": suggestionBox.node_position.y !== undefined ? suggestionBox.node_position.y + 100 : 0, - }, - "group": "nodes", - } + newitem.type = "ACTION" + newitem.isStartNode = false + newitem.data.id = uuidv4() + newitem.data.type = "ACTION" + newitem.data.isStartNode = false - newitem.type = "ACTION" - newitem.isStartNode = false - newitem.data.id = uuidv4() - newitem.data.type = "ACTION" - newitem.data.isStartNode = false + newitem.data.is_valid = true + newitem.data.isValid = true - newitem.data.is_valid = true - newitem.data.isValid = true + cy.add({ + group: newitem.group, + data: newitem.data, + position: newitem.position, + }); - cy.add({ - group: newitem.group, - data: newitem.data, - position: newitem.position, - }); + // Add edge + const newId = uuidv4() + cy.add({ + group: "edges", + data: { + id: newId, + _id: newId, + source: suggestionBox.attachedTo, + target: newitem.data.id, + } + }) + //label: "Generated", - // Add edge - const newId = uuidv4() - cy.add({ - group: "edges", - data: { - id: newId, - _id: newId, - source: suggestionBox.attachedTo, - target: newitem.data.id, - } - }) - //label: "Generated", - - setSuggestionBox({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "attachedTo": "", - }); - } - }) - .catch((error) => { - setAiQueryModalOpen(false) - setAutocompleting(false) - if (setSuggestionLoading !== undefined) { - setSuggestionLoading(false) - } + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", + }); + } + }) + .catch((error) => { + setAiQueryModalOpen(false) + setAutocompleting(false) + if (setSuggestionLoading !== undefined) { + setSuggestionLoading(false) + } - console.log("Conv response error: ", error); - }); - } + console.log("Conv response error: ", error); + }); + } @@ -4852,231 +5483,237 @@ const releaseToConnectLabel = "Release to Connect" // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once // onNodeClick const onNodeSelect = (event, newAppAuth) => { + // Forces all states to update at the same time, - // Otherwise everything is SUPER slow - - // FIXME: Do absolutely NOT use JSON.stringify on the event.target.data() - // This causes memory referencing to become a nightmare - const data = event.target.data() + // Otherwise everything is SUPER slow + + // FIXME: Do absolutely NOT use JSON.stringify on the event.target.data() + // This causes memory referencing to become a nightmare + const data = event.target.data() if (data.app_name === "Shuffle Workflow") { if ((data?.parameters !== undefined) && (data?.parameters?.length > 0)) { getWorkflowApps(data.parameters[0].value) } } - if (data.buttonType == "ACTIONSUGGESTION") { - const attachedToId = data.attachedTo + if (data.buttonType == "ACTIONSUGGESTION") { + const attachedToId = data.attachedTo - const parentitemRaw = cy.getElementById(data.attachedTo) - const parentitem = parentitemRaw.data() - if (parentitem !== null && parentitem !== undefined) { - setTimeout(() => { - parentitemRaw.select() + const parentitemRaw = cy.getElementById(data.attachedTo) + const parentitem = parentitemRaw.data() + if (parentitem !== null && parentitem !== undefined) { + setTimeout(() => { + parentitemRaw.select() - const allNodes = cy.nodes().jsons() - for (var _key in allNodes) { - const currentNode = allNodes[_key] + const allNodes = cy.nodes().jsons() + for (var _key in allNodes) { + const currentNode = allNodes[_key] - if (currentNode.data.buttonType === "ACTIONSUGGESTION") { - cy.getElementById(currentNode.data.id).remove() - } - } - }, 100) + if (currentNode.data.buttonType === "ACTIONSUGGESTION") { + cy.getElementById(currentNode.data.id).remove() + } + } + }, 100) - const findaction = data.label - console.log("CLICKED: ", findaction, apps.length) + const findaction = data.label + for (let appkey in apps) { + const curapp = apps[appkey] + if (curapp.name !== parentitem.app_name) { + continue + } - for (let appkey in apps) { - const curapp = apps[appkey] - if (curapp.name !== parentitem.app_name) { - continue - } + if (curapp.actions === undefined || curapp.actions === null) { + continue + } - if (curapp.actions === undefined || curapp.actions === null) { - continue - } + for (let actionkey in curapp.actions) { + const curaction = curapp.actions[actionkey] - for (let actionkey in curapp.actions) { - const curaction = curapp.actions[actionkey] + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + if (curaction.category_label[0].toLowerCase() === findaction.toLowerCase()) { + console.log("FOUND: ", curaction) - if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - if (curaction.category_label[0].toLowerCase() === findaction.toLowerCase()) { - console.log("FOUND: ", curaction) + // Update the action itself + // Find the action index, and update: + // - label + // - description + // - parameters + // - name - // Update the action itself - // Find the action index, and update: - // - label - // - description - // - parameters - // - name - - var foundindex = -1 - for (let wfactionkey in workflow.actions) { - const wfaction = workflow.actions[wfactionkey] - if (wfaction.id === data.attachedTo) { - foundindex = wfactionkey - break - } - } + var foundindex = -1 + for (let wfactionkey in workflow.actions) { + const wfaction = workflow.actions[wfactionkey] + if (wfaction.id === data.attachedTo) { + foundindex = wfactionkey + break + } + } - console.log("Updating action: ", foundindex, findaction) - if (foundindex >= 0) { - workflow.actions[foundindex].label = findaction - workflow.actions[foundindex].description = curaction.description - workflow.actions[foundindex].parameters = curaction.parameters - workflow.actions[foundindex].name = curaction.name + console.log("Updating action: ", foundindex, findaction) + if (foundindex >= 0) { + workflow.actions[foundindex].label = findaction + workflow.actions[foundindex].description = curaction.description + workflow.actions[foundindex].parameters = curaction.parameters + workflow.actions[foundindex].name = curaction.name - setWorkflow(workflow) - console.log(workflow) - } - break - } - } - } + setWorkflow(workflow) + console.log(workflow) + } + break + } + } + } - break - } + break + } - return - } + return + } - } else if (data.isSuggestion === true) { - console.log("Suggestion! Replace with a real action.") - - const attachedToId = data.attachedTo - //event.target.data("attachedTo", "") - - const allNodes = cy.nodes().jsons(); - for (var _key in allNodes) { - const currentNode = allNodes[_key]; - // console.log("CURRENT NODE: ", currentNode) - if ((currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== data.id) { - cy.getElementById(currentNode.data.id).remove(); - } - } - - - // Add relevant fields for the action and connect it to the parent (attachedTo) - //event.target.data("attachedTo", "") - // Decides directionality - // - const isTarget = event.target.data("isTarget") - const target = isTarget ? data.id : attachedToId - const source = isTarget ? attachedToId : data.id - const newId = uuidv4() + } else if (data.isSuggestion === true) { + console.log("Suggestion! Replace with a real action.") - // Add a new node - const newAction = { - ...data, - } + const attachedToId = data.attachedTo + //event.target.data("attachedTo", "") + + const allNodes = cy.nodes().jsons(); + for (var _key in allNodes) { + const currentNode = allNodes[_key]; + // console.log("CURRENT NODE: ", currentNode) + if ((currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== data.id) { + cy.getElementById(currentNode.data.id).remove(); + } + } + + + // Add relevant fields for the action and connect it to the parent (attachedTo) + //event.target.data("attachedTo", "") + // Decides directionality + // + const isTarget = event.target.data("isTarget") + const target = isTarget ? data.id : attachedToId + const source = isTarget ? attachedToId : data.id + const newId = uuidv4() + + // Add a new node + const newAction = { + ...data, + } newAction.attachedTo = "" - newAction.isSuggestion = false - newAction.finished = true - newAction.isButton = false - newAction.private_id = "" - newAction.type = "ACTION" - if (newAction.app_name === "Shuffle Subflow") { - newAction.type = "TRIGGER" - newAction.trigger_type = "SUBFLOW" - } else { - newAction.decorator = false - newAction.suggested = true - } + newAction.isSuggestion = false + newAction.finished = true + newAction.isButton = false + newAction.private_id = "" + newAction.type = "ACTION" + if (newAction.app_name === "Shuffle Subflow") { + newAction.type = "TRIGGER" + newAction.trigger_type = "SUBFLOW" + } else { + newAction.decorator = false + newAction.suggested = true + } - newAction.label = data.label - newAction.id = uuidv4() - // Find the app and add params? + newAction.label = data.label + newAction.id = uuidv4() + // Find the app and add params? - cy.add({ - group: "nodes", - data: newAction, - position: { - x: event.target.position().x, - y: event.target.position().y, - }, - }) + cy.add({ + group: "nodes", + data: newAction, + position: { + x: event.target.position().x, + y: event.target.position().y, + }, + }) - toast("Suggestion added!") - setTimeout(() => { - const newBranch = { - source: source, - target: newAction.id, - _id: newId, - id: newId, - decorator: false, - finished: true, - } + toast("Suggestion added!") + setTimeout(() => { + const newBranch = { + source: source, + target: newAction.id, + _id: newId, + id: newId, + decorator: false, + finished: true, + } - cy.add({ - group: "edges", - data: newBranch, - }) - - setWorkflowRecommendations(undefined) + cy.add({ + group: "edges", + data: newBranch, + }) - // Find the new node we added from newAction.id - const newActionNode = cy.getElementById(newAction.id) - if (newActionNode !== undefined && newActionNode !== null) { - newActionNode.select() - } - - aiSubmit("Fill based on previous values", undefined, undefined, newAction) - }, 1000) - return + setWorkflowRecommendations(undefined) + + // Find the new node we added from newAction.id + const newActionNode = cy.getElementById(newAction.id) + if (newActionNode !== undefined && newActionNode !== null) { + newActionNode.select() + } + + aiSubmit("Fill based on previous values", undefined, undefined, newAction) + }, 1000) + return } - ReactDOM.unstable_batchedUpdates(() => { + ReactDOM.unstable_batchedUpdates(() => { + const selectedNodes = cy.$(':selected') if (data.isButton) { - if (data.buttonType === "suggestion") { - if (cy === undefined) { - console.log("Cy not defined yet") - return - } + if (selectedNodes?.length > 1) { + event.target.unselect() + //console.log(": ", selectedNodes.length) + return + } - // Inject HTML at a fixed location? - //const newHtml = "

    Do you want to add this suggestion?

    " + if (data.buttonType === "suggestion") { + if (cy === undefined) { + console.log("Cy not defined yet") + return + } - // Find mouse cursor position on screen - console.log("Suggestion html to be added at location: ", event) - /* - const position = { - "top": cy.pan().y, - "left": cy.pan().x, - } - */ + // Inject HTML at a fixed location? + //const newHtml = "

    Do you want to add this suggestion?

    " + + // Find mouse cursor position on screen + console.log("Suggestion html to be added at location: ", event) + /* + const position = { + "top": cy.pan().y, + "left": cy.pan().x, + } + */ - const position = event.target.renderedPosition(); - const container = cy.container(); - const offset = { - left: container.offsetLeft, - top: container.offsetTop - }; - - // Calculate the actual screen position for the box - const screenPosition = { - left: position.x + offset.left - 150, - top: position.y + offset.top, - }; - - // Log the position to the console - console.log('Node screen position:', screenPosition); + const position = event.target.renderedPosition(); + const container = cy.container(); + const offset = { + left: container.offsetLeft, + top: container.offsetTop + }; - const newbox = { - "position": screenPosition, - "node_position": event.target.position(), - "open": true, - "attachedTo": data.attachedTo, - } + // Calculate the actual screen position for the box + const screenPosition = { + left: position.x + offset.left - 150, + top: position.y + offset.top, + }; - console.log("Rendered position: ", newbox.node_position) + // Log the position to the console + console.log('Node screen position:', screenPosition); - setSuggestionBox(newbox) + const newbox = { + "position": screenPosition, + "node_position": event.target.position(), + "open": true, + "attachedTo": data.attachedTo, + } + + console.log("Rendered position: ", newbox.node_position) + + setSuggestionBox(newbox) + + // Unselect + event.target.unselect(); - // Unselect - event.target.unselect(); - } else if (data.buttonType === "delete") { const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined) { @@ -5086,24 +5723,36 @@ const releaseToConnectLabel = "Release to Connect" return } else if (data.buttonType === "set_startnode" && data.type !== "TRIGGER") { - //console.log("STARTNODE") - //event.preventDefault() - //event.stopPropagation() + // Find any node that has isStartNode set to true and set it to false + const foundNodes = cy.nodes().jsons() + var relevantNodes = [] + for (var nodekey in foundNodes) { + const node = foundNodes[nodekey] + if (node.data.isStartNode === true) { + relevantNodes.push(node) + } + } - const parentNode = cy.getElementById(data.attachedTo); + const parentNode = cy.getElementById(data.attachedTo) if (parentNode !== null && parentNode !== undefined) { - var oldstartnode = cy.getElementById(workflow.start); - if ( - oldstartnode !== null && - oldstartnode !== undefined && - oldstartnode.length > 0 - ) { - try { - oldstartnode[0].data("isStartNode", false); - } catch (e) { - console.log("Startnode error: ", e); - } - } + for (var nodekey in relevantNodes) { + const node = relevantNodes[nodekey] + var oldstartnode = cy.getElementById(node.data.id); + if ( + oldstartnode !== null && + oldstartnode !== undefined && + oldstartnode.length > 0 + ) { + try { + console.log("Old startnodes: ", oldstartnode) + for(var i = 0; i < oldstartnode.length; i++) { + oldstartnode[i].data("isStartNode", false); + } + } catch (e) { + console.log("Startnode error: ", e); + } + } + } workflow.start = parentNode.data("id"); setLastSaved(false); @@ -5115,7 +5764,6 @@ const releaseToConnectLabel = "Release to Connect" return } else if (data.buttonType === "copy") { - console.log("COPY!"); // 1. Find parent // 2. Find branches for parent @@ -5180,46 +5828,46 @@ const releaseToConnectLabel = "Release to Connect" const destinationbranches = workflow.branches.filter((foundbranch) => foundbranch.destination_id === parentNode.data("id")) - - for (var sourceBranchesKey in sourcebranches) { - var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); - newbranch.id = uuidv4() - newbranch.source_id = newNodeData.id + for (var sourceBranchesKey in sourcebranches) { + var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) + newbranch.id = uuidv4() + newbranch.source_id = newNodeData.id + + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } - for (var destinationBranchesKey in destinationbranches) { - var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) + for (var destinationBranchesKey in destinationbranches) { + var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) - const sourcenode = cy.getElementById(newbranch.source_id) - if (sourcenode !== null && sourcenode !== undefined) { - const sourcedata = sourcenode.data() + const sourcenode = cy.getElementById(newbranch.source_id) + if (sourcenode !== null && sourcenode !== undefined) { + const sourcedata = sourcenode.data() - if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { - continue - } + if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { + continue + } - } + } - newbranch.id = uuidv4() - newbranch.destination_id = newNodeData.id + newbranch.id = uuidv4() + newbranch.destination_id = newNodeData.id - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } //event.target.unselect(); @@ -5229,43 +5877,56 @@ const releaseToConnectLabel = "Release to Connect" return; } else if (data.isDescriptor) { - // Find parent + // Find parent event.target.unselect(); - if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { - const parentNode = cy.getElementById(data.attachedTo) - if (parentNode !== null && parentNode !== undefined) { - setTimeout(() => { - parentNode.select() - }, 100) - } - } + if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { + const parentNode = cy.getElementById(data.attachedTo) + if (parentNode !== null && parentNode !== undefined) { + setTimeout(() => { + parentNode.select() + }, 100) + } + } //console.log("Can't select descriptor"); - if (data.isTrigger) { - console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") - setExecutionModalOpen(true) - } + if (data.isTrigger) { + console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") + setExecutionModalOpen(true) + } return; } - if (data.type === undefined) { - console.log("No type, automatically setting to action"); - data.type = "ACTION" - } + if (data.type === undefined) { + console.log("No type, automatically setting to action"); + data.type = "ACTION" + } if (data.type === "ACTION") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment({}) - // FIXME: is this what is mapping it an actual action in the workflow? wtf? - var curaction = workflow.actions.find((a) => a.id === data.id) + // FIXME: is this what is mapping it an actual action in the workflow? wtf? + var curactionIndex = workflow.actions.findIndex((a) => a.id === data.id) + var curaction = undefined + if (curactionIndex >= 0) { + curaction = workflow.actions[curactionIndex] + } + if (!curaction || curaction === undefined) { if (data.id !== undefined && data.app_name !== undefined) { workflow.actions.push(data) setWorkflow(workflow) - // FIXME: Is this necessary? + // FIXME: Is this necessary? //curaction = JSON.parse(JSON.stringify(data)) } else { if (workflow.public !== true) { @@ -5277,10 +5938,16 @@ const releaseToConnectLabel = "Release to Connect" } } - // FIXME: This change may cause... something + // FIXME: This change may have caused... something + // FIXME: Somehow there is a referencing problem between the action in + // cytoscape and the one in the "workflow.actions" state curaction = data + //workflow.actions[curactionIndex] = curaction + + //const data = event.target.data() + //event.target.data(curaction) + //event.target.data(curaction) - //var newapps = JSON.parse(JSON.stringify(apps)) var newapps = apps if (apps === null || apps === undefined || apps.length === 0) { newapps = filteredApps @@ -5293,7 +5960,7 @@ const releaseToConnectLabel = "Release to Connect" ) if (curapp === undefined || curapp === null) { - console.log("Couldn't find app with that ID - checking with name & version") + console.log(`Couldn't find app with ID '${curaction.app_id}' - checking with name & version`) curapp = newapps.find((a) => a.name === curaction.app_name && @@ -5310,65 +5977,64 @@ const releaseToConnectLabel = "Release to Connect" (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version))) ) - } + } if (curaction.template === true && curaction.name !== undefined) { //newapps. const parsedname = curaction.name.replaceAll(" ", "_").toLowerCase() - console.log("FIND AN ACTION AMONG THE APPS THAT MATCHES NAME: ", parsedname) - curaction.matching_actions = [] - for (var newAppskey in newapps) { - for (let actionsSubkey in newapps[newAppskey].actions) { - const tmpaction = newapps[newAppskey].actions[actionsSubkey] - if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { - console.log("MATCH!: ", newapps[newAppskey]) - curaction.matching_actions.push({ - "app_name": newapps[newAppskey].name, - "app_version": newapps[newAppskey].app_version, - "app_id": newapps[newAppskey].id, - "action": tmpaction, - "large_image": newapps[newAppskey].large_image, - "app_index": newAppskey, - "action_index": actionsSubkey, - }) - } - } - } - } + curaction.matching_actions = [] + for (var newAppskey in newapps) { + for (let actionsSubkey in newapps[newAppskey].actions) { + const tmpaction = newapps[newAppskey].actions[actionsSubkey] + if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { + console.log("MATCH!: ", newapps[newAppskey]) + curaction.matching_actions.push({ + "app_name": newapps[newAppskey].name, + "app_version": newapps[newAppskey].app_version, + "app_id": newapps[newAppskey].id, + "action": tmpaction, + "large_image": newapps[newAppskey].large_image, + "app_index": newAppskey, + "action_index": actionsSubkey, + }) + } + } + } + } - if (!curapp || curapp === undefined) { - // Check local storage has it - const foundapps = localStorage.getItem("apps") - if (foundapps !== null && foundapps !== undefined) { - try { - const parsedapps = JSON.parse(foundapps) - if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { - for (let appkey in parsedapps) { - if (parsedapps[appkey].name === curaction.app_name) { - curapp = parsedapps[appkey] - break - } - } - } - } catch (e) { - console.log("Problem with parsing apps from local storage", e) - } + if (!curapp || curapp === undefined) { + // Check local storage has it + const foundapps = localStorage.getItem("apps") + if (foundapps !== null && foundapps !== undefined) { + try { + const parsedapps = JSON.parse(foundapps) + if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { + for (let appkey in parsedapps) { + if (parsedapps[appkey].name === curaction.app_name) { + curapp = parsedapps[appkey] + break + } + } + } + } catch (e) { + console.log("Problem with parsing apps from local storage", e) + } - } else { - console.log("No apps found in local storage") - } - } + } else { + console.log("No apps found in local storage") + } + } - /* - if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { - toast(`Side-loading app ${curapp.name} to get actions.`) - } - */ + /* + if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { + toast(`Side-loading app ${curapp.name} to get actions.`) + } + */ - if (curapp !== undefined && curapp !== null && curapp.id !== undefined && curapp.id !== null && curapp.id.length > 0) { - loadAppConfig(curapp.id, true) - } + if (curapp !== undefined && curapp !== null && curapp.id !== undefined && curapp.id !== null && curapp.id.length > 0) { + loadAppConfig(curapp.id, true) + } if (!curapp || curapp === undefined) { const tmpapp = { @@ -5385,31 +6051,31 @@ const releaseToConnectLabel = "Release to Connect" curaction.app_id = curapp.id - if (curapp.authentication === undefined || curapp.authentication === null) { - setAuthenticationType({ - type: "", - }) + if (curapp.authentication === undefined || curapp.authentication === null) { + setAuthenticationType({ + type: "", + }) - curapp.authentication = { - type: "", - required: false, - } - } else { - setAuthenticationType( - curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { - type: curapp.authentication.type, - redirect_uri: curapp.authentication.redirect_uri, - refresh_uri: curapp.authentication.refresh_uri, - token_uri: curapp.authentication.token_uri, - scope: curapp.authentication.scope, - client_id: curapp.authentication.client_id, - client_secret: curapp.authentication.client_secret, - grant_type: curapp.authentication.grant_type, - } : { - type: "", - } - ) - } + curapp.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { + type: curapp.authentication.type, + redirect_uri: curapp.authentication.redirect_uri, + refresh_uri: curapp.authentication.refresh_uri, + token_uri: curapp.authentication.token_uri, + scope: curapp.authentication.scope, + client_id: curapp.authentication.client_id, + client_secret: curapp.authentication.client_secret, + grant_type: curapp.authentication.grant_type, + } : { + type: "", + } + ) + } const requiresAuth = curapp.authentication.required; //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) setRequiresAuthentication(requiresAuth); @@ -5428,57 +6094,57 @@ const releaseToConnectLabel = "Release to Connect" const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); - const curappName = curapp.name.toLowerCase() - for (let tmpAuthKey in tmpAuth) { - var item = tmpAuth[tmpAuthKey]; + const curappName = curapp.name.toLowerCase() + for (let tmpAuthKey in tmpAuth) { + var item = tmpAuth[tmpAuthKey]; - const newfields = {}; - if (item.app.name.toLowerCase() !== curappName) { - continue - } + const newfields = {}; + if (item.app.name.toLowerCase() !== curappName) { + continue + } - // Makes list into key:value object - for (let fieldkey in item.fields) { - if (item.fields[fieldkey] === undefined) { - console.log("Problem with filterkey in Node select", fieldkey) - continue - } + // Makes list into key:value object + for (let fieldkey in item.fields) { + if (item.fields[fieldkey] === undefined) { + console.log("Problem with filterkey in Node select", fieldkey) + continue + } - const filterkey = item.fields[fieldkey]["key"] - if (filterkey === null || filterkey === undefined) { - console.log("Problem with filterkey 2. Null or undefined 3") - continue - } + const filterkey = item.fields[fieldkey]["key"] + if (filterkey === null || filterkey === undefined) { + console.log("Problem with filterkey 2. Null or undefined 3") + continue + } - newfields[filterkey] = item.fields[fieldkey]["value"]; - } + newfields[filterkey] = item.fields[fieldkey]["value"]; + } - item.fields = newfields; - if (item.app.name.toLowerCase() === curappName) { - authenticationOptions.push(item); - if (item.id === findAuthId) { - curaction.selectedAuthentication = item; - } - } - } + item.fields = newfields; + if (item.app.name.toLowerCase() === curappName) { + authenticationOptions.push(item); + if (item.id === findAuthId) { + curaction.selectedAuthentication = item; + } + } + } - // Find with authenticationOption (authenticationOptions) has the highest .edited time. In this index, set the "last_modified" to true - - var latesttime = 0 - var latestindex = -1 + // Find with authenticationOption (authenticationOptions) has the highest .edited time. In this index, set the "last_modified" to true - for (var i = 0; i < authenticationOptions.length; i++) { - const authopt = authenticationOptions[i] + var latesttime = 0 + var latestindex = -1 - if (authopt.edited > latesttime) { - latesttime = authopt.edited - latestindex = i - } - } + for (var i = 0; i < authenticationOptions.length; i++) { + const authopt = authenticationOptions[i] - if (latestindex !== -1) { - authenticationOptions[latestindex].last_modified = true - } + if (authopt.edited > latesttime) { + latesttime = authopt.edited + latestindex = i + } + } + + if (latestindex !== -1) { + authenticationOptions[latestindex].last_modified = true + } curaction.authentication = authenticationOptions if ( @@ -5495,80 +6161,82 @@ const releaseToConnectLabel = "Release to Connect" curaction.selectedAuthentication = {}; } - if ( - curaction.parameters !== undefined && - curaction.parameters !== null && - curaction.parameters.length > 0 - ) { - for (var curActionParamKey in curaction.parameters) { - if ( - curaction.parameters[curActionParamKey].options !== undefined && - curaction.parameters[curActionParamKey].options !== null && - curaction.parameters[curActionParamKey].options.length > 0 && - curaction.parameters[curActionParamKey].value === "" - ) { - curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; - } - } + if ( + curaction.parameters !== undefined && + curaction.parameters !== null && + curaction.parameters.length > 0 + ) { + for (var curActionParamKey in curaction.parameters) { + if ( + curaction.parameters[curActionParamKey].options !== undefined && + curaction.parameters[curActionParamKey].options !== null && + curaction.parameters[curActionParamKey].options.length > 0 && + curaction.parameters[curActionParamKey].value === "" + ) { + curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; + } + } - } else { - console.log("Should check APP if it has the same params as ACTION") - for (let actionKey in curapp.actions) { - const tmpaction = curapp.actions[actionKey] - if (tmpaction.name === curaction.name) { - console.log("Found action - needs change?", tmpaction) - if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { - curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) - } - break - } - } + } else { + console.log("Should check APP if it has the same params as ACTION") + for (let actionKey in curapp.actions) { + const tmpaction = curapp.actions[actionKey] + if (tmpaction.name === curaction.name) { + console.log("Found action - needs change?", tmpaction) + if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { + curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) + } + break + } + } - } + } - // Fix authentication fields that may be missing in the UI - if (curapp.authentication.required && !curapp?.authentication?.type?.includes("oauth")) { - if (curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) { - var actionChanged = false - for (let paramKey in curapp.authentication.parameters) { - var param = curapp.authentication.parameters[paramKey] + // Fix authentication fields that may be missing in the UI + if (curapp.authentication.required && !curapp?.authentication?.type?.includes("oauth")) { + if (curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) { + var actionChanged = false + for (let paramKey in curapp.authentication.parameters) { + var param = curapp.authentication.parameters[paramKey] - if (curaction.parameters === undefined || curaction.parameters === null) { - curaction.parameters = [] - } + if (curaction.parameters === undefined || curaction.parameters === null) { + curaction.parameters = [] + } - var found = false - for (let actionParamKey in curaction.parameters) { - if (curaction.parameters[actionParamKey].name === param.name) { - found = true - break - } - } + var found = false + for (let actionParamKey in curaction.parameters) { + if (curaction.parameters[actionParamKey].name === param.name) { + found = true + break + } + } - if (!found) { - param.configuration = true - curaction.parameters.push(param) - - actionChanged = true - } - } + if (!found) { + param.configuration = true + curaction.parameters.push(param) - if (actionChanged && workflow.actions !== undefined && workflow.actions !== null) { - // Find it in the workflow and set it - for (let wfActionKey in workflow.actions) { - if (workflow.actions[wfActionKey].id === curaction.id) { - workflow.actions[wfActionKey] = curaction - } - } + actionChanged = true + } + } - setWorkflow(workflow) - - } - } - } + if (actionChanged && workflow.actions !== undefined && workflow.actions !== null) { + // Find it in the workflow and set it + for (let wfActionKey in workflow.actions) { + if (workflow.actions[wfActionKey].id === curaction.id) { + workflow.actions[wfActionKey] = curaction + } + } - setSelectedApp(curapp) - setSelectedAction(curaction) + setWorkflow(workflow) + + } + } + } + + setTimeout(() => { + setSelectedApp(curapp) + setSelectedAction(curaction) + }, 50) cy.removeListener("drag"); cy.removeListener("free"); @@ -5591,6 +6259,14 @@ const releaseToConnectLabel = "Release to Connect" setSelectedActionEnvironment(env); } } else if (data.type === "TRIGGER") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment({}) if (workflow.triggers === null) { workflow.triggers = [] @@ -5601,142 +6277,205 @@ const releaseToConnectLabel = "Release to Connect" ) if (trigger_index === -1) { - workflow.triggers.push(data) - trigger_index = workflow.triggers.length - 1 - setWorkflow(workflow) + + // Don't do this in suborg workflows. + if (originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null) { + toast("RE-adding missing trigger node onclick") + workflow.triggers.push(data) + trigger_index = workflow.triggers.length - 1 + setWorkflow(workflow) + } + + for (var triggerkey in workflow.triggers) { + const curtrigger = workflow.triggers[triggerkey] + if (curtrigger.name === data.name) { + trigger_index = triggerkey + break + } + } } if (data.app_name === "Shuffle Workflow" || data.app_name === "User Input") { - // Check if public workflow - if (workflow.public === true) { - setWorkflows([workflow]) - } else { - getAvailableWorkflows(trigger_index); - getSettings(); + // Check if public workflow + if (workflow.public === true) { + setWorkflows([workflow]) + } else { + getAvailableWorkflows(trigger_index); + } + } else if (data.app_name === "Schedule") { + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0 && originalWorkflow.org_id !== undefined && originalWorkflow.org_id !== null && originalWorkflow.org_id.length > 0 && workflow.org_id === originalWorkflow.org_id) { + // Allows a parent workflow to control the schedule + } else if (data.replacement_for_trigger !== undefined && data.replacement_for_trigger !== null && data.replacement_for_trigger.length > 0) { + toast.warning("This schedule is controlled by the parent workflow. If you want additional schedule control, please add a custom schedule to this workflow.", { + autoClose: 30000, + }) + event.target.unselect() + return } - } else if (data.app_name === "Webhook") { + + } else if (data.app_name === "Webhook" && trigger_index >= 0) { + if (workflow.triggers[trigger_index] !== undefined && workflow.triggers[trigger_index] !== null && + ( workflow.triggers[trigger_index].parameters === undefined || + workflow.triggers[trigger_index].parameters === null || + workflow.triggers[trigger_index].parameters.length === 0) + ) { + workflow.triggers[trigger_index].parameters = [ + { + name: "url", + value: referenceUrl + "webhook_" + selectedTrigger.id, + }, + { + name: "tmp", + value: "webhook_" + selectedTrigger.id, + }, + { + name: "auth_headers", + value: "", + }, + { + name: "custom_response_body", + value: "", + }, + { + name: "await_response", + value: "v1", + }, + ] + } + if (workflow.triggers[trigger_index].parameters !== undefined && workflow.triggers[trigger_index].parameters !== null && workflow.triggers[trigger_index].parameters.length > 0) { + workflow.triggers[trigger_index].parameters[0] = { name: "url", value: referenceUrl + "webhook_" + workflow.triggers[trigger_index].id, }; - if (workflow.triggers[trigger_index].parameters.length < 5) { - console.log("Adding to webhook params!") - workflow.triggers[trigger_index].parameters.push({ - name: "await_response", - value: "v1," - }) - } + if (workflow.triggers[trigger_index].parameters.length < 5) { + console.log("Adding to webhook params!") + workflow.triggers[trigger_index].parameters.push({ + name: "await_response", + value: "v1," + }) + } + + //workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined && } - } else if (data.app_name === "Pipeline") { + } else if (data.app_name === "Pipeline" && trigger_index >= 0) { - // Check if environment is set - if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { - for (var envKey in environments) { - if (environments[envKey].archived === true) { - continue - } + // Check if environment is set + if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { + for (var envKey in environments) { + if (environments[envKey].archived === true) { + continue + } - if (environments[envKey].Name.toLowerCase() === "cloud") { - continue - } + if (environments[envKey].Name.toLowerCase() === "cloud") { + continue + } - workflow.triggers[trigger_index].environment = environments[envKey].Name - data.environment = environments[envKey].Name - //setSelectedTrigger(data) - break - } - } - } + workflow.triggers[trigger_index].environment = environments[envKey].Name + data.environment = environments[envKey].Name + //setSelectedTrigger(data) + break + } + } + } setTimeout(() => { - if (trigger_index !== -1) { - const trigger = workflow.triggers[trigger_index] - if (trigger !== undefined && trigger !== null) { + if (trigger_index !== -1) { + const trigger = workflow.triggers[trigger_index] + if (trigger !== undefined && trigger !== null) { - // Autofixer - if (trigger.trigger_type === "USERINPUT") { - const relevantparams = [ - "alertinfo", - "options", - "type", - "email", - "sms", - "subflow", - ] - var foundparams = 0 - for (var paramkey in trigger.parameters) { - if (relevantparams.includes(trigger.parameters[paramkey].name)) { - foundparams++ - } - } + // Autofixer + if (trigger.trigger_type === "USERINPUT") { + const relevantparams = [ + "alertinfo", + "options", + "type", + "email", + "sms", + "subflow", + ] + var foundparams = 0 + for (var paramkey in trigger.parameters) { + if (relevantparams.includes(trigger.parameters[paramkey].name)) { + foundparams++ + } + } - if (foundparams < 6) { - trigger.parameters = [{ - name: "alertinfo", - value: "Do you want to continue the workflow? Start parameters: $exec", - },{ - name: "options", - value: "boolean", - }, - { - name: "type", - value: "subflow", - }, - { - name: "email", - value: "test@test.com", - }, - { - name: "sms", - value: "0000000", - }, - { - name: "subflow", - value: "", - }] + if (foundparams < 6) { + trigger.parameters = [{ + name: "alertinfo", + value: "Do you want to continue the workflow? Start parameters: $exec", + }, { + name: "options", + value: "boolean", + }, + { + name: "type", + value: "subflow", + }, + { + name: "email", + value: "test@test.com", + }, + { + name: "sms", + value: "0000000", + }, + { + name: "subflow", + value: "", + }] - workflow.triggers[trigger_index].parameters = trigger.parameters - } - } - } - } + workflow.triggers[trigger_index].parameters = trigger.parameters + } + } + } + } - if (allTriggers !== undefined && allTriggers !== null) { + if (allTriggers !== undefined && allTriggers !== null) { - // Just checking all three. Could just make a new list, but meh - if (allTriggers.pipelines !== undefined && allTriggers.pipelines !== null) { - for (var pipelineKey in allTriggers.pipelines) { - if (allTriggers.pipelines[pipelineKey].id === data.id) { - data.status = allTriggers.pipelines[pipelineKey].status - } - } - } + // Just checking all three. Could just make a new list, but meh + if (allTriggers.pipelines !== undefined && allTriggers.pipelines !== null) { + for (var pipelineKey in allTriggers.pipelines) { + if (allTriggers.pipelines[pipelineKey].id === data.id) { + data.status = allTriggers.pipelines[pipelineKey].status + } + } + } - if (allTriggers.webhooks !== undefined && allTriggers.webhooks !== null) { - for (var webhookKey in allTriggers.webhooks) { - if (allTriggers.webhooks[webhookKey].id === data.id) { - data.status = allTriggers.webhooks[webhookKey].status - } - } - } + if (allTriggers.webhooks !== undefined && allTriggers.webhooks !== null) { + for (var webhookKey in allTriggers.webhooks) { + if (allTriggers.webhooks[webhookKey].id === data.id) { + data.status = allTriggers.webhooks[webhookKey].status + } + } + } - if (allTriggers.schedules !== undefined && allTriggers.schedules !== null) { - for (var scheduleKey in allTriggers.schedules) { - if (allTriggers.schedules[scheduleKey].id === data.id) { - data.status = allTriggers.schedules[scheduleKey].status - } - } - } - } + if (allTriggers.schedules !== undefined && allTriggers.schedules !== null) { + for (var scheduleKey in allTriggers.schedules) { + if (allTriggers.schedules[scheduleKey].id === data.id) { + data.status = allTriggers.schedules[scheduleKey].status + } + } + } + } - setSelectedTriggerIndex(trigger_index) - setSelectedTrigger(data) - //setSelectedActionEnvironment(data.env) - }, 25) + setSelectedTriggerIndex(trigger_index) + setSelectedTrigger(data) + //setSelectedActionEnvironment(data.env) + }, 25) } else if (data.type === "COMMENT") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment(data); } else { toast("Can't handle node type " + data.type); @@ -5751,14 +6490,14 @@ const releaseToConnectLabel = "Release to Connect" selected: "", }); - setSuggestionBox({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "attachedTo": "", - }); + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", + }); sendStreamRequest({ "item": "node", @@ -5794,8 +6533,8 @@ const releaseToConnectLabel = "Release to Connect" toast("Failed to auto-activate the app. Go to /apps and activate it.") } else { if (refresh === true) { - setHighlightedApp(appid) - //toast("App activated for your organisation! Refresh the page to use the app.") + setHighlightedApp(appid) + //toast("App activated for your organisation! Refresh the page to use the app.") getApps() } @@ -5958,7 +6697,7 @@ const releaseToConnectLabel = "Release to Connect" selectedkey = `.${key}`; } - for (let [subitem,subitemval] in Object.entries(value)) { + for (let [subitem, subitemval] in Object.entries(value)) { toreturn = GetParamMatch( paramname, value[subitem], @@ -6035,7 +6774,7 @@ const releaseToConnectLabel = "Release to Connect" if (parents.length > 1) { for (let parentkey in parents) { const item = parents[parentkey]; - if (item.label === "Execution Argument") { + if (item.label === "Runtime Argument") { continue; } @@ -6045,34 +6784,34 @@ const releaseToConnectLabel = "Release to Connect" : item.label.toLowerCase().trim().replaceAll(" ", "_"); exampledata = GetExampleResult(item); - if (dstdata.parameters !== undefined && dstdata.parameters !== null) { - for (let [paramkey,paramkeyval] in Object.entries(dstdata.parameters)) { - const param = dstdata.parameters[paramkey]; - // Skip authentication params - if (param.configuration) { - continue - } + if (dstdata.parameters !== undefined && dstdata.parameters !== null) { + for (let [paramkey, paramkeyval] in Object.entries(dstdata.parameters)) { + const param = dstdata.parameters[paramkey]; + // Skip authentication params + if (param.configuration) { + continue + } - if (param.options !== undefined && param.options !== null && param.options.length > 0) { - continue - } + if (param.options !== undefined && param.options !== null && param.options.length > 0) { + continue + } - const paramname = param.name - .toLowerCase() - .trim() - .replaceAll("_", " "); + const paramname = param.name + .toLowerCase() + .trim() + .replaceAll("_", " "); - const foundresult = GetParamMatch(paramname, exampledata, ""); - if (foundresult.length > 0) { - if (dstdata.parameters[paramkey].value.length === 0) { - dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; - dstdata.parameters[paramkey].autocompleted = true - } else { - //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; - } - } + const foundresult = GetParamMatch(paramname, exampledata, ""); + if (foundresult.length > 0) { + if (dstdata.parameters[paramkey].value.length === 0) { + dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + dstdata.parameters[paramkey].autocompleted = true + } else { + //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + } + } } - } + } } } @@ -6085,7 +6824,7 @@ const releaseToConnectLabel = "Release to Connect" const edge = event.target.data(); if (edge.source === undefined && edge.target === undefined) { - //console.log("Edge added without source or target") + //console.log("Edge added without source or target") return } @@ -6101,14 +6840,14 @@ const releaseToConnectLabel = "Release to Connect" const destinationnode = cy.getElementById(edge.target) if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { - console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) + console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) } else { - if (sourcenode.data("name") === "switch") { - event.target.remove() - return - } + if (sourcenode.data("name") === "switch") { + event.target.remove() + return + } - console.log("Edge added: Is it a trigger? If so, check if it already has a branch and remove it: ", sourcenode.data()) + //console.log("Edge added: Is it a trigger? If so, check if it already has a branch and remove it: ", sourcenode.data()) if (sourcenode.data("type") === "TRIGGER") { if (sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { setTimeout(() => { @@ -6137,8 +6876,8 @@ const releaseToConnectLabel = "Release to Connect" const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) const currentedge = cy.getElementById(edge.id) if (currentedge !== undefined && currentedge !== null) { - currentedge.style('control-point-distance', edgeCurve.distance) - currentedge.style('control-point-weight', edgeCurve.weight) + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) } } @@ -6148,7 +6887,7 @@ const releaseToConnectLabel = "Release to Connect" ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { - console.log("User Input or Shuffle Workflow") + console.log("User Input or Shuffle Workflow") } else { toast("Can't have triggers as target of branch") event.target.remove() @@ -6156,7 +6895,6 @@ const releaseToConnectLabel = "Release to Connect" } const eventTarget = event.target.target() - console.log("BUTTON ADDED! Find parent from: ", eventTarget) if (eventTarget.data("isButton") === true) { const parentNode = cy.getElementById(eventTarget.data("attachedTo")) event.target.remove() @@ -6194,48 +6932,58 @@ const releaseToConnectLabel = "Release to Connect" // dest == source && source == dest // dest == dest && source == source // backend: check all children? to stop recursion - // + // var found = false; for (let branchkey in workflow.branches) { if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) { - toast("A branch in the opposite direction already exists") - event.target.remove() - found = true - break - } - if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { - - console.log("That branch already exists: ", workflow.branches[branchkey]) + // Find the branch as well const foundbranch = cy.getElementById(workflow.branches[branchkey].id) if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { - console.log("Removing branch: ", foundbranch.data()) - + toast("A branch in the opposite direction already exists") event.target.remove() - found = true break - } else { - //console.log("Old branch didn't exist afterall. Remove.") - } - } + } + } - if (edge.target === workflow.start) { + if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { + + console.log("That branch already exists: ", workflow.branches[branchkey]) + const foundbranch = cy.getElementById(workflow.branches[branchkey].id) + if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { + console.log("Removing branch: ", foundbranch.data()) + + event.target.remove() + + found = true + break + } else { + //console.log("Old branch didn't exist afterall. Remove.") + } + } + + if (edge.target === workflow.start) { targetnode = workflow.triggers.findIndex( (data) => data.id === edge.source - ); + ) + if (targetnode === -1) { if (targetnode.type !== "TRIGGER") { - toast("Can't make arrow to starting node"); - event.target.remove(); - break; + console.log("SOURCENODE: ", sourcenode.data()) + if (sourcenode.data("type") === "TRIGGER" && sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { + } else { + toast("Can't make branch to starting node"); + event.target.remove() + break + } } found = true; } - } + } - if (edge.source === workflow.branches[branchkey].source_id) { + if (edge.source === workflow.branches[branchkey].source_id) { // FIXME: Verify multi-target for triggers // 1. Check if destination exists // 2. Check if source is a trigger @@ -6311,7 +7059,6 @@ const releaseToConnectLabel = "Release to Connect" const node = event.target; const nodedata = JSON.parse(JSON.stringify(event.target.data())) - if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined)) { return } @@ -6340,21 +7087,21 @@ const releaseToConnectLabel = "Release to Connect" setWorkflowAsCode(true); } - if (nodedata.decorator !== true && nodedata.attachedTo === undefined) { - var newdata = JSON.parse(JSON.stringify(nodedata)) - newdata.large_image = "" - sendStreamRequest({ - "item": "node", - "type": "add", - "id": nodedata.id, - "data": nodedata, - "x": node.position("x"), - "y": node.position("y"), - }) - } + if (nodedata.decorator !== true && nodedata.attachedTo === undefined) { + var newdata = JSON.parse(JSON.stringify(nodedata)) + newdata.large_image = "" + sendStreamRequest({ + "item": "node", + "type": "add", + "id": nodedata.id, + "data": nodedata, + "x": node.position("x"), + "y": node.position("y"), + }) + } if (nodedata.type === "ACTION") { - // Should get recommendations to load in for all nodesma + // Should get recommendations to load in for all nodesma if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) { const newEdgeUuid = uuidv4(); @@ -6371,7 +7118,6 @@ const releaseToConnectLabel = "Release to Connect" data: newcybranch, }; - console.log("SHOULD STITCH WITH STARTNODE"); cy.add(edgeToBeAdded); } @@ -6397,7 +7143,7 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.parameters !== undefined && nodedata.parameters !== null && !nodedata?.label?.endsWith("_copy")) { var newparameters = []; - for (let [subkey,subkeyval] in Object.entries(nodedata.parameters)) { + for (let [subkey, subkeyval] in Object.entries(nodedata.parameters)) { var newparam = JSON.parse(JSON.stringify(nodedata.parameters[subkey])) newparam.id = uuidv4() @@ -6419,13 +7165,13 @@ const releaseToConnectLabel = "Release to Connect" workflow.actions.push(nodedata); } - // 1. Check how many actions there are. If less than three, send a toast notification with suggested workflows - //if (workflow.actions.length < 3) { - // toast("Recommendations to show??") - //} + // 1. Check how many actions there are. If less than three, send a toast notification with suggested workflows + //if (workflow.actions.length < 3) { + // toast("Recommendations to show??") + //} setWorkflow(workflow); - fetchRecommendations(workflow) + fetchRecommendations(workflow) } else if (nodedata.type === "TRIGGER") { if (nodedata.is_valid === false) { toast("This trigger is not available to you"); @@ -6456,15 +7202,13 @@ const releaseToConnectLabel = "Release to Connect" data: newcybranch, }; - if (edgeToBeAdded.data.source !== edgeToBeAdded.data.target && edgeToBeAdded.data.source !== undefined && edgeToBeAdded.data.target !== undefined) { - if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { - console.log("NAME: ", nodedata.name) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - console.log("Edge handle: ", edgeToBeAdded) - cy.add(edgeToBeAdded); - } - } - } + if (edgeToBeAdded.data.source !== edgeToBeAdded.data.target && edgeToBeAdded.data.source !== undefined && edgeToBeAdded.data.target !== undefined) { + if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { + if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { + cy.add(edgeToBeAdded) + } + } + } setWorkflow(workflow); } @@ -6492,42 +7236,42 @@ const releaseToConnectLabel = "Release to Connect" // Check if the source is trigger and can start //console.log("Removed: ", edge.data()) const allNodes = cy.nodes().jsons() - for (let nodekey in allNodes) { - const curnode = allNodes[nodekey] - if (curnode.data.type !== "TRIGGER") { - continue - } + for (let nodekey in allNodes) { + const curnode = allNodes[nodekey] + if (curnode.data.type !== "TRIGGER") { + continue + } - if (curnode.data.id === edge.data("source")) { - console.log("Found matching trigger source: ", curnode) - if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { + if (curnode.data.id === edge.data("source")) { + console.log("Found matching trigger source: ", curnode) + if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { - // If it's started, READD the edge - if (curnode.data.status === "running") { - //console.log("Edge is running - readd it: ", edge.data()) + // If it's started, READD the edge + if (curnode.data.status === "running") { + //console.log("Edge is running - readd it: ", edge.data()) - // Just making sure it's not running infinitely - var newdata = edge.data() - newdata.readded = true + // Just making sure it's not running infinitely + var newdata = edge.data() + newdata.readded = true - try { - cy.add({ - group: "edges", - data: newdata, - }) + try { + cy.add({ + group: "edges", + data: newdata, + }) - //toast.error("You must STOP the trigger before deleting its branches") - console.log("You must STOP the trigger before deleting its branches") - } catch (e) { - console.log("Failed re-adding edge: ", e) - } - } + //toast.error("You must STOP the trigger before deleting its branches") + console.log("You must STOP the trigger before deleting its branches") + } catch (e) { + console.log("Failed re-adding edge: ", e) + } + } - //status: "uninitialized", - } - } - } + //status: "uninitialized", + } + } + } workflow.branches = workflow.branches.filter( (a) => a.id !== edge.data().id @@ -6560,14 +7304,14 @@ const releaseToConnectLabel = "Release to Connect" const node = event.target; const data = node.data(); - // FIXME: This is still a bit buggy - if (data.decorator !== true && data.attachedTo === undefined) { - sendStreamRequest({ - "item": "node", - "type": "remove", - "id": data.id, - }) - } + // FIXME: This is still a bit buggy + if (data.decorator !== true && data.attachedTo === undefined) { + sendStreamRequest({ + "item": "node", + "type": "remove", + "id": data.id, + }) + } if (data.finished === false) { return @@ -6672,7 +7416,7 @@ const releaseToConnectLabel = "Release to Connect" } console.log("CTRL+C"); - if (cy !== undefined) { + if (cy !== undefined && cy !== null) { var cydata = cy.$(":selected").jsons(); if (cydata !== undefined && cydata !== null && cydata.length > 0) { console.log(cydata); @@ -6739,26 +7483,21 @@ const releaseToConnectLabel = "Release to Connect" }; const handlePaste = (event) => { - //console.log("EV: ", event) if ( event.path !== undefined && event.path !== null && event.path.length > 0 ) { - //console.log("PATH: ", event.path[0]) if (event.path[0].localName !== "body") { - //console.log("Skipping because body is not targeted") return; } } - //console.log("PATH2: ", event.target) if ( event.target !== undefined && event.target !== null ) { if (event.target.localName !== "body") { - //console.log("Skipping because body is not targeted") return; } } @@ -6768,46 +7507,61 @@ const releaseToConnectLabel = "Release to Connect" const clipboard = (event.originalEvent || event).clipboardData.getData( "text/plain" ); - //console.log("Text: ", clipboard) - //window.document.execCommand('insertText', false, text); - // + try { + const allnodes = cy.nodes().jsons() var parsedjson = JSON.parse(clipboard); - // Check if array - if (!Array.isArray(parsedjson)) { - console.log("Not array! Adding to array.") - parsedjson = [parsedjson] - } + if (!Array.isArray(parsedjson)) { + console.log("Not array! Adding to array.") + parsedjson = [parsedjson] + } for (let jsonkey in parsedjson) { var item = parsedjson[jsonkey]; - console.log("Adding: ", item); - if (item.data === undefined || item.data === null) { - console.log("Appending from here") - const newitem = { - "data": item, - "position": { - "x": 0, - "y": 0 - }, - "group": "nodes", - } + if (item.data === undefined || item.data === null) { + console.log("Appending from here") + const newitem = { + "data": item, + "position": { + "x": 0, + "y": 0 + }, + "group": "nodes", + } - item = newitem - item.type = "ACTION" - item.isStartNode = false - item.data.type = "ACTION" - item.data.isStartNode = false - } + item = newitem + item.type = "ACTION" + item.isStartNode = false + item.data.type = "ACTION" + item.data.isStartNode = false + } - item.data.id = uuidv4() + // Find a cy.data() label with the same name + const foundnodes = allnodes.filter((data) => { + //console.log("COMP: ", data.data.label, item.data.label) + if (data.data.label === undefined || data.data.label === null) { + return false + } + + return data.data.label === item.data.label + }) + + if (foundnodes !== undefined && foundnodes !== null && foundnodes.length > 0) { + // Weird naming copy lol + item.data.label = item.data.label + "_copy_" + allnodes.length + } + + item.data.id = uuidv4() cy.add({ group: item.group, data: item.data, - position: item.position, - }); + position: { + x: item.position.x+20, + y: item.position.y+20, + }, + }) } } catch (e) { console.log("Error pasting: ", e); @@ -6820,15 +7574,15 @@ const releaseToConnectLabel = "Release to Connect" document.addEventListener("paste", handlePaste); }; - const getEnvironments = (orgId) => { - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } + const getEnvironments = (orgId, defaultEnvironmentName) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } fetch(globalUrl + "/api/v1/getenvironments", { method: "GET", @@ -6864,7 +7618,9 @@ const releaseToConnectLabel = "Release to Connect" } } - if (showEnvCnt > 1) { + // Always showing for now + //if (showEnvCnt > 1) { + if (showEnvCnt > 0) { setShowEnvironment(true) } @@ -6872,7 +7628,7 @@ const releaseToConnectLabel = "Release to Connect" for (let jsonkey in responseJson) { if (!responseJson[jsonkey].archived) { setDefaultEnvironmentIndex(jsonkey) - break; + break } } } @@ -6887,22 +7643,16 @@ const releaseToConnectLabel = "Release to Connect" setEnvironments(responseJson) } - /* - setTimeout(() => { - console.log("ACTIONS: ", workflow.actions) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - for (var actionkey in workflow.actions) { - if (workflow.actions[actionkey].environment !== undefined && workflow.actions[actionkey].environment !== null && workflow.actions[actionkey].environment.length > 0) { + if (defaultEnvironmentName !== undefined && defaultEnvironmentName !== null && defaultEnvironmentName.length > 0 && responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + const env = responseJson.findIndex((data) => data.Name === defaultEnvironmentName) + if (env !== -1) { + setSelectedActionEnvironment(responseJson[env]) - const env = environments.findIndex((data) => data.Name === workflow.actions[actionkey].environment) - if (env !== -1) { - setSelectedActionEnvironment(environments[env]) - } - } - } + if (originalSelectedEnvironment === undefined || originalSelectedEnvironment === null || Object.keys(originalSelectedEnvironment).length === 0) { + setOriginalSelectedEnvironment(responseJson[env]) + } } - }, 2500) - */ + } }) .catch((error) => { //toast(error.toString()); @@ -6920,14 +7670,14 @@ const releaseToConnectLabel = "Release to Connect" workflow.id.length > 0 ) { - // Check if - if (distributedFromParent === "" && suborgWorkflows === []) { - toast.info("Redirecting as the workflow ID does not match the URL") + // Check if + if (distributedFromParent === "" && suborgWorkflows === []) { + toast.info("Redirecting as the workflow ID does not match the URL") - setTimeout(() => { - window.location.pathname = "/workflows/" + props.match.params.key; - }, 2500) - } + setTimeout(() => { + window.location.pathname = "/workflows/" + props.match.params.key; + }, 2500) + } } const animationDuration = 150; @@ -6939,37 +7689,37 @@ const releaseToConnectLabel = "Release to Connect" cytoscapeElement.style.cursor = "default" } - if (nodedata.finished === false) { + if (nodedata.finished === false) { - // Should just be 1, so this should be fast enough :3 - const incomingEdges = event.target.incomers("edge").jsons() - if (incomingEdges !== undefined && incomingEdges !== null) { - for (var i = 0; i < incomingEdges.length; i++) { - // Find the actual edge - const edge = cy.getElementById(incomingEdges[i].data.id) - if (edge === undefined || edge === null) { - console.log("edge is null or undefined") - continue - } + // Should just be 1, so this should be fast enough :3 + const incomingEdges = event.target.incomers("edge").jsons() + if (incomingEdges !== undefined && incomingEdges !== null) { + for (var i = 0; i < incomingEdges.length; i++) { + // Find the actual edge + const edge = cy.getElementById(incomingEdges[i].data.id) + if (edge === undefined || edge === null) { + console.log("edge is null or undefined") + continue + } - // Set the edge to be dashed - edge.style("target-arrow-color", "#555555") - edge.style("line-style", "dashed") - edge.style("line-gradient-stop-colors", ["#555555", "#555555"]) - } - } + // Set the edge to be dashed + edge.style("target-arrow-color", "#555555") + edge.style("line-style", "dashed") + edge.style("line-gradient-stop-colors", ["#555555", "#555555"]) + } + } return } - if (nodedata.name === "switch") { - return - } + if (nodedata.name === "switch") { + return + } // console.log("nodedata", nodedata); // console.log("nodedata.app_name: ", nodedata.app_name); if (nodedata.app_name !== undefined) { - + const allNodes = cy.nodes().jsons(); // console.log("allNodes: ", allNodes) for (var nodekey in allNodes) { @@ -6977,9 +7727,9 @@ const releaseToConnectLabel = "Release to Connect" // console.log("Current node: ", currentNode); if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { - if (currentNode.data.buttonType === "condition-drag") { - continue - } + if (currentNode.data.buttonType === "condition-drag") { + continue + } cy.getElementById(currentNode.data.id).remove(); } @@ -6989,7 +7739,7 @@ const releaseToConnectLabel = "Release to Connect" // Skipping node editing if it's the selected one - if (cy !== undefined) { + if (cy !== undefined && cy !== null) { const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { const item = typeIds[idkey] @@ -7001,6 +7751,10 @@ const releaseToConnectLabel = "Release to Connect" //if (nodedata.id === selectedAction.id || nodedata.id === selectedTrigger.id) { // return //} + // + if (nodedata.name === "switch" || nodedata.app_id === "shuffle_agent") { + return + } var parsedStyle = { "border-width": "1px", @@ -7079,156 +7833,162 @@ const releaseToConnectLabel = "Release to Connect" }; const addRunCountButton = (event) => { - // Count executions? - // Maybe it shouldn't be onclick? - } + // Count executions? + // Maybe it shouldn't be onclick? + } const addConditionDraggers = (event, allElements, branches) => { - const nodedata = event.target.data() - const position = event.target.position() + const nodedata = event.target.data() + const position = event.target.position() - var conditions = [] - const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") + var conditions = [] + const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") - try { - conditions = JSON.parse(foundParam.value) - } catch (e) { - //toast("Failed parsing conditions: ", e) - } + try { + conditions = JSON.parse(foundParam.value) + } catch (e) { + //toast("Failed parsing conditions: ", e) + } - // Test conditions - if (conditions === undefined || conditions === null || typeof conditions !== "object") { - return - } + // Test conditions + if (conditions === undefined || conditions === null || typeof conditions !== "object") { + return + } - // Look for if it has the "Else" condition or not - const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") - const parentId = nodedata.id + // Look for if it has the "Else" condition or not + const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") + const parentId = nodedata.id - // Force following of Else at the least - const newId = uuidv5(parentId, uuidv5.URL) - if (elseindex === -1) { - conditions.push({ - name: "Else", - check: "Else", - id: newId, - parent_source: parentId, - }) - } else { - conditions[elseindex].id = newId - } + // Force following of Else at the least + const newId = uuidv5(parentId, uuidv5.URL) + if (elseindex === -1) { + conditions.push({ + name: "Else", + check: "Else", + id: newId, + parent_source: parentId, + }) + } else { + conditions[elseindex].id = newId + } - // 4 conditions (with else) = 300px -> 75px each - const parentHeight = (conditions.length*75)*0.75 + // 4 conditions (with else) = 300px -> 75px each + const parentHeight = (conditions.length * 75) * 0.75 - var startheight = -parentHeight/2 - var newnodes = [] - for (let conditionkey in conditions) { - var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id + var startheight = -parentHeight / 2 + var newnodes = [] + for (let conditionkey in conditions) { + var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id - // Check if circleId is a valid uuid or not - if (circleId === undefined || circleId === null) { - circleId = uuidv4() - } + // Check if circleId is a valid uuid or not + if (circleId === undefined || circleId === null) { + circleId = uuidv4() + } - if (!isUUID(circleId)) { - if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { - circleId = uuidv5(conditions[circleId].name, uuidv5.URL) - } else { - circleId = uuidv4() - conditions[conditionkey].name = circleId - conditions[conditionkey].id = circleId - } - } + if (!isUUID(circleId)) { + if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { + circleId = uuidv5(conditions[circleId].name, uuidv5.URL) + } else { + circleId = uuidv4() + conditions[conditionkey].name = circleId + conditions[conditionkey].id = circleId + } + } - // Check if circleId already exists as a node - if (cy !== undefined && cy !== null) { - const existingNode = cy.getElementById(circleId) - if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { - continue - } - } + // Check if circleId already exists as a node + if (cy !== undefined && cy !== null) { + const existingNode = cy.getElementById(circleId) + if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { + continue + } + } - // 1. Create "small" nodes at each point along the section based on the amount of conditions - // 2. Make these conditions have edgehandles - // 3. Make these conditions have a "drag" handle - const px = position.x + 65 - const py = position.y + startheight + // 1. Create "small" nodes at each point along the section based on the amount of conditions + // 2. Make these conditions have edgehandles + // 3. Make these conditions have a "drag" handle + const px = position.x + 65 + const py = position.y + startheight - console.log("Y height: ", startheight) + console.log("Y height: ", startheight) - const node = { - group: "nodes", - data: { - name: conditions[conditionkey].name, - id: circleId, - buttonType: "condition-drag", - attachedTo: nodedata.id, - is_valid: true, - }, - position: { - x: px, - y: py, - }, - locked: true, - } + const node = { + group: "nodes", + data: { + name: conditions[conditionkey].name, + id: circleId, + buttonType: "condition-drag", + attachedTo: nodedata.id, + is_valid: true, + }, + position: { + x: px, + y: py, + }, + locked: true, + } - newnodes.push(node) + newnodes.push(node) - // Check if ANY of the incoming branches has the id as source - if (branches !== undefined && branches !== null && branches.length > 0) { - for (let branchkey in branches) { - const branch = branches[branchkey] - if (branch.source_id !== circleId) { - continue - } + // Check if ANY of the incoming branches has the id as source + if (branches !== undefined && branches !== null && branches.length > 0) { + for (let branchkey in branches) { + const branch = branches[branchkey] + if (branch.source_id !== circleId) { + continue + } - const branchid = uuidv4() - newnodes.push({ - group: "edges", - data: { - id: branchid, - _id: branchid, + const branchid = uuidv4() + newnodes.push({ + group: "edges", + data: { + id: branchid, + _id: branchid, - source: circleId, - target: branch.destination_id, - label: branch.label, - conditions: branch.conditions, - hasErrors: branch.has_errors, - decorator: false, - parent_source: parentId, - } - }) - } - } + source: circleId, + target: branch.destination_id, + label: branch.label, + conditions: branch.conditions, + hasErrors: branch.has_errors, + decorator: false, + parent_source: parentId, + } + }) + } + } - startheight = startheight + parentHeight/(conditions.length-1) - } + startheight = startheight + parentHeight / (conditions.length - 1) + } - if (cy !== undefined && cy !== null) { - cy.add(newnodes) - } else { - var newelements = elements - if (allElements !== undefined) { - newelements = allElements - } + if (cy !== undefined && cy !== null) { + cy.add(newnodes) + } else { + var newelements = elements + if (allElements !== undefined) { + newelements = allElements + } - for (let nodekey in newnodes) { - newelements.push(newnodes[nodekey]) - } + for (let nodekey in newnodes) { + newelements.push(newnodes[nodekey]) + } - console.log("ELEMENTS: ", newelements) - setElements(newelements) - } + console.log("ELEMENTS: ", newelements) + setElements(newelements) + } } const addCopyButton = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - const px = parentNode.position("x") - 65; - const py = parentNode.position("y") - 5; + var xDiff = 0 + var yDiff = 0 + if (parentNode.data("app_id") === "shuffle_agent") { + xDiff = 70 + } + + const px = parentNode.position("x") - 65 - xDiff + const py = parentNode.position("y") - 5 - yDiff const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); @@ -7261,296 +8021,302 @@ const releaseToConnectLabel = "Release to Connect" }; const addActionSuggestions = (nodedata, event) => { - if (nodedata.type !== "ACTION") { - return - } + if (nodedata.type !== "ACTION") { + return + } - var parentNode = cy.$("#" + event.target.data("id")) - if (parentNode.data("isButton") || parentNode.data("buttonId")) { - return - } + var parentNode = cy.$("#" + event.target.data("id")) + if (parentNode.data("isButton") || parentNode.data("buttonId")) { + return + } - const px = parentNode.position("x") + 0; - const py = parentNode.position("y") + 100; + const px = parentNode.position("x") + 0; + const py = parentNode.position("y") + 100; - const parentlabel = parentNode.data("label")?.toLowerCase().replace(" ", "_") - const parentname = parentNode.data("app_name")?.toLowerCase().replace(" ", "_") - if (!parentlabel?.startsWith(parentname)+"_") { - return - } + const parentlabel = parentNode.data("label")?.toLowerCase().replace(" ", "_") + const parentname = parentNode.data("app_name")?.toLowerCase().replace(" ", "_") + if (!parentlabel?.startsWith(parentname) + "_") { + return + } - // Check if action has changed - const parentAppId = parentNode.data("app_id") - const parentActionname = parentNode.data("name") - for (var appkey in apps) { - const curapp = apps[appkey] + // Check if action has changed + const parentAppId = parentNode.data("app_id") + const parentActionname = parentNode.data("name") + for (var appkey in apps) { + const curapp = apps[appkey] - if (curapp.id !== parentAppId) { - continue - } + if (curapp.id !== parentAppId) { + continue + } - if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { - continue - } + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } - var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) - if (startIndex === -1) { - startIndex = 0 - } + var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) + if (startIndex === -1) { + startIndex = 0 + } - if (curapp.actions[startIndex].name !== parentActionname) { - console.log("Return 2") - return - } + if (curapp.actions[startIndex].name !== parentActionname) { + console.log("Return 2") + return + } - break - } + break + } - console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") + console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") - const iconInfo = { - icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", - iconColor: buttonColor, - iconBackgroundColor: buttonBackgroundColor, - }; + const iconInfo = { + icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - // 1. Find the app - // 2. Loop the apps' actions - // 3. Find actions based on category label IF it exists - - var addedLabels = [] - for (let appKey in apps) { - const curapp = apps[appKey] - if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { - continue - } + // 1. Find the app + // 2. Loop the apps' actions + // 3. Find actions based on category label IF it exists - if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { - continue - } + var addedLabels = [] + for (let appKey in apps) { + const curapp = apps[appKey] + if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { + continue + } - for (let actionKey in curapp.actions) { - const curaction = curapp.actions[actionKey] - - // Check if this is the current action already - if (parentNode.data("name") == curaction.name) { - continue - } + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } - if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - if (addedLabels.includes(curaction.category_label[0])) { - continue - } + for (let actionKey in curapp.actions) { + const curaction = curapp.actions[actionKey] - if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { - continue - } + // Check if this is the current action already + if (parentNode.data("name") == curaction.name) { + continue + } - cy.add({ - group: "nodes", - data: { - weight: 30, - id: uuidv4(), - label: curaction.category_label[0], - attachedTo: event.target.data("id"), - is_valid: true, - buttonType: "ACTIONSUGGESTION", - }, - position: { - x: px, - y: py + (addedLabels.length * 50), - }, - locked: true, - }) + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + if (addedLabels.includes(curaction.category_label[0])) { + continue + } - addedLabels.push(curaction.category_label[0]) - if (addedLabels.length >= 2) { - break - } - } - } + if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { + continue + } - break - } + cy.add({ + group: "nodes", + data: { + weight: 30, + id: uuidv4(), + label: curaction.category_label[0], + attachedTo: event.target.data("id"), + is_valid: true, + buttonType: "ACTIONSUGGESTION", + }, + position: { + x: px, + y: py + (addedLabels.length * 50), + }, + locked: true, + }) + + addedLabels.push(curaction.category_label[0]) + if (addedLabels.length >= 2) { + break + } + } + } + + break + } } const addSuggestionButtons = (nodedata, event) => { - //console.log("Skipping Adding suggestion buttons") - //return - // Skipping add for now. Should Re-enable + //console.log("Skipping Adding suggestion buttons") + //return + // Skipping add for now. Should Re-enable - // Add a button for autocompletion based on input - if (nodedata.type === "ACTION") { - /* - const color = "#34a853" + // Add a button for autocompletion based on input + if (nodedata.type === "ACTION") { + /* + const color = "#34a853" + + // Fix icon + const iconInfo = { + icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const decoratorNode = { + position: { + x: event.target.position().x + 0, + y: event.target.position().y + 65, + }, + locked: true, + data: { + isButton: true, + isValid: true, + is_valid: true, + //label: "+", + attachedTo: nodedata.id, + imageColor: color, + buttonType: "suggestion", + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + }, + }; + + cy.add(decoratorNode); + */ + } - // Fix icon - const iconInfo = { - icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", - iconColor: buttonColor, - iconBackgroundColor: buttonBackgroundColor, - }; + if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { + return + } - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - const decoratorNode = { - position: { - x: event.target.position().x + 0, - y: event.target.position().y + 65, - }, - locked: true, - data: { - isButton: true, - isValid: true, - is_valid: true, - //label: "+", - attachedTo: nodedata.id, - imageColor: color, - buttonType: "suggestion", - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - }, - }; + const px = parentNode.position("x") + 0; + const py = parentNode.position("y") + 200; + const circleId = (newNodeId = uuidv4()); - cy.add(decoratorNode); - */ - } + parentNode.data("circleId", circleId); - if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { - return - } + var startHeight = 0 + for (let recKey in workflowRecommendations) { + const rec = workflowRecommendations[recKey] + if (rec.action_id !== nodedata.id) { + continue + } - var parentNode = cy.$("#" + event.target.data("id")); - if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + if (rec.recommendations === undefined || rec.recommendations === null || rec.recommendations.length === 0) { + continue + } - const px = parentNode.position("x") + 0; - const py = parentNode.position("y") + 200; - const circleId = (newNodeId = uuidv4()); + for (let recIndex in rec.recommendations) { + const parsedRec = rec.recommendations[recIndex] + console.log("REC: ", parsedRec) - parentNode.data("circleId", circleId); + const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" + const foundApp = apps.find((app) => app.app_name === parsedRec.app_name && app.app_version === foundVersion) + // Find out if foundApp is shuffle tools, and if so, add the correct image based on name - var startHeight = 0 - for (let recKey in workflowRecommendations) { - const rec = workflowRecommendations[recKey] - if (rec.action_id !== nodedata.id) { - continue - } + const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : foundApp === undefined || foundApp === null ? theme.palette.defaultImage : foundApp.large_image - if (rec.recommendations === undefined || rec.recommendations === null || rec.recommendations.length === 0) { - continue - } + const uuid = uuidv4() + const attachedToId = event.target.data("id") + // Check if parsedRec.app_action exists already as a node under this one + const branches = cy.edges().jsons() + var found = false + for (let branchKey in branches) { + const branch = branches[branchKey] + if (branch.data.source !== attachedToId) { + continue + } - for (let recIndex in rec.recommendations) { - const parsedRec = rec.recommendations[recIndex] - console.log("REC: ", parsedRec) + const targetNode = cy.getElementById(branch.data.target) + if (targetNode === undefined || targetNode === null) { + continue + } - const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" - const foundApp = apps.find((app) => app.app_name === parsedRec.app_name && app.app_version === foundVersion) - // Find out if foundApp is shuffle tools, and if so, add the correct image based on name + if (targetNode.data("name") === parsedRec.app_action) { + console.log("Found existing node (action name): ", targetNode) + found = true + } - const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : foundApp === undefined || foundApp === null ? theme.palette.defaultImage : foundApp.large_image + // FIXME: This could potentially be removed + if (targetNode.data("app_id") === parsedRec.app_id) { + console.log("Found existing node (id): ", targetNode) + found = true + } + } - const uuid = uuidv4() - const attachedToId = event.target.data("id") - // Check if parsedRec.app_action exists already as a node under this one - const branches = cy.edges().jsons() - var found = false - for (let branchKey in branches) { - const branch = branches[branchKey] - if (branch.data.source !== attachedToId) { - continue - } + // Skip the suggestion if it already exists + if (found) { + continue + } - const targetNode = cy.getElementById(branch.data.target) - if (targetNode === undefined || targetNode === null) { - continue - } + // Checks for src/dst (e.g. trigger = src usually) + const isTarget = true - if (targetNode.data("name") === parsedRec.app_action) { - console.log("Found existing node (action name): ", targetNode) - found = true - } + var name = parsedRec.app_action + if (parsedRec.app_action === "subflow") { + name = "Shuffle Workflow" + } else if (parsedRec.app_action === "user_input") { + name = "User Input" + } - // FIXME: This could potentially be removed - if (targetNode.data("app_id") === parsedRec.app_id) { - console.log("Found existing node (id): ", targetNode) - found = true - } - } + const newaction = { + name: name, + label: parsedRec.app_action, + label_replaced: parsedRec.app_action.replace("_", " ", -1), - // Skip the suggestion if it already exists - if (found) { - continue - } + id: uuid, + app_name: parsedRec.app_name, + app_version: foundVersion, + app_id: parsedRec.app_id, + sharing: false, + private_id: "", + isStartNode: false, + large_image: largeImage, + is_valid: true, + isSuggestion: true, + isTarget: isTarget, + attachedTo: attachedToId, - // Checks for src/dst (e.g. trigger = src usually) - const isTarget = true + finished: false, + } - var name = parsedRec.app_action - if (parsedRec.app_action === "subflow") { - name = "Shuffle Workflow" - } else if (parsedRec.app_action === "user_input") { - name = "User Input" - } + cy.add({ + group: "nodes", + data: newaction, + position: { + x: px + startHeight, + y: py, + }, + locked: true, + }); - const newaction = { - name: name, - label: parsedRec.app_action, - label_replaced: parsedRec.app_action.replace("_", " ", -1), + cy.add({ + group: "edges", + data: { + source: event.target.data("id"), + target: uuid, + decorator: true, + } + }) - id: uuid, - app_name: parsedRec.app_name, - app_version: foundVersion, - app_id: parsedRec.app_id, - sharing: false, - private_id: "", - isStartNode: false, - large_image: largeImage, - is_valid: true, - isSuggestion: true, - isTarget: isTarget, - attachedTo: attachedToId, + startHeight += 100 + } - finished: false, - } - - cy.add({ - group: "nodes", - data: newaction, - position: { - x: px+startHeight, - y: py, - }, - locked: true, - }); - - cy.add({ - group: "edges", - data: { - source: event.target.data("id"), - target: uuid, - decorator: true, - } - }) - - startHeight += 100 - } - - console.log("Got Rec: ", rec) - break - } + console.log("Got Rec: ", rec) + break + } } const addDeleteButton2 = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - const px = parentNode.position("x") + 100; - const py = parentNode.position("y") + 35; + var xDiff = 0 + var yDiff = 0 + if (parentNode.data("app_id") === "shuffle_agent") { + xDiff = 70 + } + + const px = parentNode.position("x") + 100 - xDiff; + const py = parentNode.position("y") + 35 - yDiff; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); @@ -7584,8 +8350,14 @@ const releaseToConnectLabel = "Release to Connect" var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - const px = parentNode.position("x") - 65; - const py = parentNode.position("y") + 35; + var xDiff = 0 + var yDiff = 0 + if (parentNode.data("app_id") === "shuffle_agent") { + xDiff = 70 + } + + const px = parentNode.position("x") - 65 - xDiff; + const py = parentNode.position("y") + 35 - yDiff; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); @@ -7631,25 +8403,23 @@ const releaseToConnectLabel = "Release to Connect" }) if (nodedata.finished === false) { - console.log("NODE UNFINISHED (hover in): ", JSON.parse(JSON.stringify(nodedata))) + // Should just be 1, so this should be fast enough :3 + const incomingEdges = event.target.incomers("edge").jsons() + if (incomingEdges !== undefined && incomingEdges !== null) { + for (var i = 0; i < incomingEdges.length; i++) { + // Find the actual edge + const edge = cy.getElementById(incomingEdges[i].data.id) + if (edge === undefined || edge === null) { + console.log("edge is null or undefined") + continue + } - // Should just be 1, so this should be fast enough :3 - const incomingEdges = event.target.incomers("edge").jsons() - if (incomingEdges !== undefined && incomingEdges !== null) { - for (var i = 0; i < incomingEdges.length; i++) { - // Find the actual edge - const edge = cy.getElementById(incomingEdges[i].data.id) - if (edge === undefined || edge === null) { - console.log("edge is null or undefined") - continue - } - - // Set the edge to be dashed - edge.style("target-arrow-color", "white") - edge.style("line-style", "solid") - edge.style("line-gradient-stop-colors", ["white", "white"]) - } - } + // Set the edge to be dashed + edge.style("target-arrow-color", "white") + edge.style("line-style", "solid") + edge.style("line-gradient-stop-colors", ["white", "white"]) + } + } return } @@ -7660,58 +8430,58 @@ const releaseToConnectLabel = "Release to Connect" //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (nodedata.app_name !== undefined && !workflow.public === true) { - const allNodes = cy.nodes().jsons(); + const allNodes = cy.nodes().jsons(); - if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - var found = false; - for (let nodekey in allNodes) { - const currentNode = allNodes[nodekey]; - if ( - currentNode.data.attachedTo === nodedata.id && - currentNode.data.isDescriptor - ) { - found = true; - break; - } - } + if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule") { + var found = false; + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if ( + currentNode.data.attachedTo === nodedata.id && + currentNode.data.isDescriptor + ) { + found = true; + break; + } + } - if (!found) { - // Find how many executions it has - var executions = 0 - const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) - const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" - const decoratorNode = { - position: { - x: event.target.position().x + 44, - y: event.target.position().y + 44, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - isTrigger: true, - label: `${matchingExecutions.length}`, - attachedTo: nodedata.id, - imageColor: color, - hasExecutions: true, - }, - }; + if (!found) { + // Find how many executions it has + var executions = 0 + const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) + const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" + const decoratorNode = { + position: { + x: event.target.position().x + 44, + y: event.target.position().y + 44, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + isTrigger: true, + label: `${matchingExecutions.length}`, + attachedTo: nodedata.id, + imageColor: color, + hasExecutions: true, + }, + }; - cy.add(decoratorNode) - } - } + cy.add(decoratorNode) + } + } var found = false; for (var _key in allNodes) { - const currentNode = allNodes[_key]; + const currentNode = allNodes[_key] // console.log("CURRENT NODE: ", currentNode) - + if ((currentNode.data.buttonType === "ACTIONSUGGESTION" || currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { - if (currentNode.data.buttonType === "condition-drag") { - continue - } + if (currentNode.data.buttonType === "condition-drag") { + continue + } cy.getElementById(currentNode.data.id).remove() } @@ -7724,14 +8494,14 @@ const releaseToConnectLabel = "Release to Connect" }*/ if (currentNode.data.isButton && currentNode.data.attachedTo === nodedata.id) { - found = true; + found = true; } } - if (nodedata.name === "switch") { - addConditionDraggers(event) - return - } + if (nodedata.name === "switch") { + addConditionDraggers(event) + return + } if (!found) { addDeleteButton(event) @@ -7740,31 +8510,34 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { addCopyButton(event); } else { - // Check how many executions from the source - addRunCountButton(event); + // Check how many executions from the source + addRunCountButton(event); + } + } else { + + addCopyButton(event); + + if (nodedata.app_id !== "shuffle_agent") { + addStartnodeButton(event); } - } else { + } - addCopyButton(event); - addStartnodeButton(event); - } + // autocomplete + // right click + // suggestions + addActionSuggestions(nodedata, event); - // autocomplete - // right click - // suggestions - addActionSuggestions(nodedata, event); - - if (workflow.actions.length < 4) { - addSuggestionButtons(nodedata, event); - } else { - //console.log("Too many actions to suggest (for now)") - } - } + if (workflow.actions.length < 4) { + addSuggestionButtons(nodedata, event); + } else { + //console.log("Too many actions to suggest (for now)") + } + } } - if (nodedata.name === "switch") { - return - } + if (nodedata.name === "switch" || nodedata.app_id === "shuffle_agent") { + return + } var parsedStyle = { "border-width": "7px", @@ -7773,9 +8546,9 @@ const releaseToConnectLabel = "Release to Connect" //"cursor": "pointer", } - if (nodedata.buttonType === "ACTIONSUGGESTION") { - parsedStyle["font-size"] = "18px" - } + if (nodedata.buttonType === "ACTIONSUGGESTION") { + parsedStyle["font-size"] = "18px" + } const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { @@ -7789,7 +8562,7 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.type !== "COMMENT") { parsedStyle.color = "white"; - } + } if (event.target !== undefined && event.target !== null) { event.target.animate( @@ -7822,10 +8595,10 @@ const releaseToConnectLabel = "Release to Connect" const edgeData = event.target.data(); if (edgeData.decorator === true) { - // Defaults - event.target.style("target-arrow-color", "#555555") - event.target.style("line-style", "dashed") - event.target.style("line-gradient-stop-colors", ["#555555", "#555555"]) + // Defaults + event.target.style("target-arrow-color", "#555555") + event.target.style("line-style", "dashed") + event.target.style("line-gradient-stop-colors", ["#555555", "#555555"]) return; } @@ -7846,15 +8619,15 @@ const releaseToConnectLabel = "Release to Connect" const edgeData = event.target.data(); if (edgeData.decorator === true) { - // Set color of it to white and not stripled - event.target.style("target-arrow-color", "white") - event.target.style("line-style", "solid") - event.target.style("line-gradient-stop-colors", ["white", "white"]) + // Set color of it to white and not stripled + event.target.style("target-arrow-color", "white") + event.target.style("line-style", "solid") + event.target.style("line-gradient-stop-colors", ["white", "white"]) return; } - // FIXME: Color problem. Do later + // FIXME: Color problem. Do later //sendStreamRequest({ // "item": "edge", // "type": "hover", @@ -7911,9 +8684,9 @@ const releaseToConnectLabel = "Release to Connect" if (event.target !== undefined && event.target !== null) { - // If decorator and hovered - // Set color to white - + // If decorator and hovered + // Set color to white + @@ -7981,19 +8754,19 @@ const releaseToConnectLabel = "Release to Connect" } } */ - if (isNaN(controlPointDistance[0])) { - controlPointDistance[0] = 0 - } - if (isNaN(controlPointDistance[1])) { - controlPointDistance[1] = 0 - } + if (isNaN(controlPointDistance[0])) { + controlPointDistance[0] = 0 + } + if (isNaN(controlPointDistance[1])) { + controlPointDistance[1] = 0 + } - if (isNaN(controlPointWeight[0])) { - controlPointWeight[0] = 0 - } - if (isNaN(controlPointWeight[1])) { - controlPointWeight[1] = 0 - } + if (isNaN(controlPointWeight[0])) { + controlPointWeight[0] = 0 + } + if (isNaN(controlPointWeight[1])) { + controlPointWeight[1] = 0 + } return { "distance": controlPointDistance, @@ -8002,32 +8775,32 @@ const releaseToConnectLabel = "Release to Connect" } const setupGraph = (inputworkflow) => { - // Reset cytoscape nodes and branches - if (cy !== undefined && cy !== null) { - if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { - //cy.remove('*') - } - } + // Reset cytoscape nodes and branches + if (cy !== undefined && cy !== null) { + if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { + //cy.remove('*') + } + } - if (inputworkflow.actions === undefined || inputworkflow.actions === null) { - inputworkflow.actions = [] - } + if (inputworkflow.actions === undefined || inputworkflow.actions === null) { + inputworkflow.actions = [] + } - if (inputworkflow.branches === undefined || inputworkflow.branches === null) { - inputworkflow.branches = [] - } + if (inputworkflow.branches === undefined || inputworkflow.branches === null) { + inputworkflow.branches = [] + } - if (inputworkflow.triggers === undefined || inputworkflow.triggers === null) { - inputworkflow.triggers = [] - } + if (inputworkflow.triggers === undefined || inputworkflow.triggers === null) { + inputworkflow.triggers = [] + } - if (inputworkflow.comments === undefined || inputworkflow.comments === null) { - inputworkflow.comments = [] - } + if (inputworkflow.comments === undefined || inputworkflow.comments === null) { + inputworkflow.comments = [] + } - if (inputworkflow.visual_branches === undefined || inputworkflow.visual_branches === null) { - inputworkflow.visual_branches = [] - } + if (inputworkflow.visual_branches === undefined || inputworkflow.visual_branches === null) { + inputworkflow.visual_branches = [] + } const actions = inputworkflow.actions.map((action) => { const node = {}; @@ -8049,13 +8822,13 @@ const releaseToConnectLabel = "Release to Connect" action.iconBackground = iconInfo.iconBackgroundColor; } } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { - const iconInfo = GetIconInfo(action) - if (iconInfo !== undefined && iconInfo !== null) { - action.fillGradient = iconInfo.fillGradient - action.iconBackground = iconInfo.iconBackgroundColor - action.fillstyle = "linear-gradient" - } - } + const iconInfo = GetIconInfo(action) + if (iconInfo !== undefined && iconInfo !== null) { + action.fillGradient = iconInfo.fillGradient + action.iconBackground = iconInfo.iconBackgroundColor + action.fillstyle = "linear-gradient" + } + } node.position = action.position; node.data = action; @@ -8065,10 +8838,10 @@ const releaseToConnectLabel = "Release to Connect" node.data.type = "ACTION"; node.isStartNode = action["id"] === inputworkflow.start; - if (node.data.errors !== undefined && node.data.errors !== null && node.data.errors.length > 0) { - node.data.is_valid = false - node.is_valid = false - } + if (node.data.errors !== undefined && node.data.errors !== null && node.data.errors.length > 0) { + node.data.is_valid = false + node.is_valid = false + } if (inputworkflow.public === true) { node.data.is_valid = true @@ -8087,150 +8860,210 @@ const releaseToConnectLabel = "Release to Connect" node.data.example = example; - if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.actions !== undefined && originalWorkflow.actions !== null && originalWorkflow.actions.length > 0) { - // Find the node in the original workflow - var inParent = false - for (var i = 0; i < originalWorkflow.actions.length; i++) { - const originalAction = originalWorkflow.actions[i] - if (originalAction.id === action.id) { - inParent = true - break - } - } + if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.actions !== undefined && originalWorkflow.actions !== null && originalWorkflow.actions.length > 0) { + // Find the node in the original workflow + var inParent = false + for (var i = 0; i < originalWorkflow.actions.length; i++) { + const originalAction = originalWorkflow.actions[i] + if (originalAction.id === action.id) { + inParent = true + break + } + } - if (inParent === true) { - setTimeout(() => { - const foundnode = cy.getElementById(action.id) - if (foundnode !== undefined && foundnode !== null) { - const parsedStyle = { - "border-width": "3px", - "border-opacity": "1", - "border-color": "#40E0D0", - "opacity": "0.4", - } + if (inParent === true) { + setTimeout(() => { + const foundnode = cy.getElementById(action.id) + if (foundnode !== undefined && foundnode !== null) { + const parsedStyle = { + "border-width": "3px", + "border-opacity": "1", + "border-color": "#40E0D0", + "opacity": "0.4", + } - const animationDuration = 150 - foundnode.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } - }, 500) - } - } + const animationDuration = 150 + foundnode.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + }, 500) + } + } return node }) - // What are these again? Where are they used? - const decoratorNodes = [] + // What are these again? Where are they used? + const decoratorNodes = [] - /* - // Removed for now as it wasn't really that helpful - const decoratorNodes = inputworkflow.actions.map((action) => { - if (!action.isStartNode) { - if (action.app_name === "Testing") { - return null - } else if (action.app_name === "Shuffle Tools") { - return null - } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { - return null + /* + // Removed for now as it wasn't really that helpful + const decoratorNodes = inputworkflow.actions.map((action) => { + if (!action.isStartNode) { + if (action.app_name === "Testing") { + return null + } else if (action.app_name === "Shuffle Tools") { + return null + } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { + return null + } } + + if (action.id === undefined || action.id === null) { + return null } - - if (action.id === undefined || action.id === null) { - return null - } - - if (action.position === undefined || action.position === null || action.position.x === undefined || action.position.x === null || action.position.y === undefined || action.position.y === null) { - return null - } - - const iconInfo = GetIconInfo(action); - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - - const offset = action.isStartNode ? 36 : 44; - - const decoratorNode = { - position: { - x: action.position.x + offset, - y: action.position.y + offset, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - label: "", - image: svgpin_Url, - imageColor: iconInfo.iconBackgroundColor, - attachedTo: action.id, - }, + + if (action.position === undefined || action.position === null || action.position.x === undefined || action.position.x === null || action.position.y === undefined || action.position.y === null) { + return null } - return decoratorNode - }) - */ + + const iconInfo = GetIconInfo(action); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const offset = action.isStartNode ? 36 : 44; + + const decoratorNode = { + position: { + x: action.position.x + offset, + y: action.position.y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: action.id, + }, + } + return decoratorNode + }) + */ const foundtriggers = inputworkflow.triggers.map((trigger) => { const node = {}; node.position = trigger.position; - if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) { - - // Search triggers array for it where the name is matching and set image - var foundTrigger = triggers.find((t) => t.name === trigger.name) - if (foundTrigger !== undefined && foundTrigger !== null) { - console.log("Autofilled missing trigger image") - trigger.large_image = foundTrigger.large_image - } - } + // Search triggers array for it where the name is matching and set image + if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) { + var foundTrigger = triggers.find((t) => t.name === trigger.name) + if (foundTrigger !== undefined && foundTrigger !== null) { + console.log("Autofilled missing trigger image") + trigger.large_image = foundTrigger.large_image + } + } node.data = trigger; node.data._id = trigger["id"]; node.data.id = trigger["id"]; node.data.type = "TRIGGER"; - if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.triggers !== undefined && originalWorkflow.triggers !== null && originalWorkflow.triggers.length > 0) { - // Find the node in the original workflow - var inParent = false - for (var i = 0; i < originalWorkflow.triggers.length; i++) { - const originalAction = originalWorkflow.triggers[i] - if (originalAction.id === trigger.id) { + // Adds the correct branching for same-workflow trigger + if (trigger?.trigger_type === "SUBFLOW" && trigger?.parameters !== undefined && trigger?.parameters !== null && trigger?.parameters.length > 0) { + var foundTargetNode = "" + var sameWorkflow = false + for (var key in trigger.parameters) { + if (trigger.parameters[key].name === "workflow" && trigger.parameters[key].value === inputworkflow.id) { + sameWorkflow = true + } + + if (trigger.parameters[key].name === "startnode" && trigger.parameters[key].value !== "") { + foundTargetNode = trigger.parameters[key].value + } + } + + if (sameWorkflow && foundTargetNode !== "") { + const newid = uuidv4() + const newbranch = { + id: newid, + _id: newid, + source: trigger.id, + source_id: trigger.id, + target: foundTargetNode, + destination_id: foundTargetNode, + + conditions: [], + has_errors: false, + decorator: true, + label: "Subflow", + } + + if (inputworkflow.visual_branches !== undefined) { + if (inputworkflow.visual_branches === null) { + inputworkflow.visual_branches = [newbranch] + } else if (inputworkflow.visual_branches.length === 0) { + inputworkflow.visual_branches.push(newbranch) + } else { + const foundIndex = inputworkflow.visual_branches.findIndex( + (branch) => branch.source_id === newbranch.source_id + ) + + if (foundIndex !== -1) { + //console.log("Already found subflow branch") + } else { + inputworkflow.visual_branches.push(newbranch); + } + } + } else { + inputworkflow.visual_branches = [newbranch] + } + + } + + } + + if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.triggers !== undefined && originalWorkflow.triggers !== null && originalWorkflow.triggers.length > 0) { + + // Find the node in the original workflow + var inParent = false + for (var i = 0; i < originalWorkflow.triggers.length; i++) { + const originalAction = originalWorkflow.triggers[i] + if (originalAction.id === trigger.id) { + inParent = true + break + } + } + + if (inParent === false) { + if (trigger.parent_controlled === true) { inParent = true - break } } - if (inParent === true) { - setTimeout(() => { - const foundnode = cy.getElementById(trigger.id) - if (foundnode !== undefined && foundnode !== null) { - const parsedStyle = { - "border-width": "3px", - "border-opacity": "1", - "border-color": "#40E0D0", - "opacity": "0.4", - } + if (inParent === true) { + setTimeout(() => { + const foundnode = cy.getElementById(trigger.id) + if (foundnode !== undefined && foundnode !== null) { + const parsedStyle = { + "border-width": "3px", + "border-opacity": "1", + "border-color": "#40E0D0", + "opacity": "0.4", + } - const animationDuration = 150 - foundnode.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } - }, 500) - } - } + const animationDuration = 150 + foundnode.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + }, 500) + } + } return node; }); @@ -8270,30 +9103,30 @@ const releaseToConnectLabel = "Release to Connect" label = conditions.length + " conditions"; } - // Verify if branch.source_id and branch.destination_id exists in triggers or actions - /* - var sourceExists = false; - var destinationExists = false; - for (var i = 0; i < insertedNodes.length; i++) { - console.log("Insertednode: ", insertedNodes[i].data); - if (insertedNodes[i].data._id === branch.source_id) { - sourceExists = true; - } - if (insertedNodes[i].data._id === branch.destination_id) { - destinationExists = true; - } - } + // Verify if branch.source_id and branch.destination_id exists in triggers or actions + /* + var sourceExists = false; + var destinationExists = false; + for (var i = 0; i < insertedNodes.length; i++) { + console.log("Insertednode: ", insertedNodes[i].data); + if (insertedNodes[i].data._id === branch.source_id) { + sourceExists = true; + } + if (insertedNodes[i].data._id === branch.destination_id) { + destinationExists = true; + } + } + + if (sourceExists === false || destinationExists === false) { + console.log("Couldn't find source node for branch " + branch.id); + return null; + } + */ - if (sourceExists === false || destinationExists === false) { - console.log("Couldn't find source node for branch " + branch.id); - return null; - } - */ - - var parentcontrolled = false - if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { - parentcontrolled = true - } + var parentcontrolled = false + if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { + parentcontrolled = true + } edge.data = { id: branch.id, @@ -8304,7 +9137,7 @@ const releaseToConnectLabel = "Release to Connect" conditions: conditions, hasErrors: branch.has_errors, decorator: false, - parent_controlled: parentcontrolled, + parent_controlled: parentcontrolled, } // This is an attempt at prettier edges. The numbers are weird to work with. @@ -8388,35 +9221,40 @@ const releaseToConnectLabel = "Release to Connect" insertedNodes = insertedNodes.concat(newedges); setWorkflow(inputworkflow); - // Reset view for cytoscape - if (cy !== undefined && cy !== null) { - cy.add(insertedNodes); - cy.fit(null, 400); - } else { - setElements(insertedNodes); - } + // Reset view for cytoscape + if (cy !== undefined && cy !== null) { + cy.add(insertedNodes) - const additionalNodes = inputworkflow.actions.map((action) => { - // Looking for: el.data("name") != "switch" - if (action.name !== "switch") { - return null - } + try { + cy.fit(null, 250) + } catch (error) { + console.log("Error fitting cytoscape (3): ", error) + } + } else { + setElements(insertedNodes) + } - addConditionDraggers({ - target: { - // Run data() function - data: function() { - return action - }, - position: function() { - return action.position - } - } - }, - insertedNodes, - inputworkflow.branches, - ) - }) + const additionalNodes = inputworkflow.actions.map((action) => { + // Looking for: el.data("name") != "switch" + if (action.name !== "switch") { + return null + } + + addConditionDraggers({ + target: { + // Run data() function + data: function () { + return action + }, + position: function () { + return action.position + } + } + }, + insertedNodes, + inputworkflow.branches, + ) + }) } const removeNode = (nodeId) => { @@ -8474,28 +9312,13 @@ const releaseToConnectLabel = "Release to Connect" if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { toast("This node can't be deleted."); } else { - selectedNode.remove(); + selectedNode.remove(); - setSelectedTrigger({}) - setSelectedEdge({}) - setSelectedAction({}) + setSelectedTrigger({}) + setSelectedEdge({}) + setSelectedAction({}) } - - // An attempt at NOT unselecting when removing - /* - setTimeout(() => { - if (parsedSelection.data() !== undefined) { - if (parsedSelection.data("id") !== selectedNode.data("id")) { - console.log("SHOULD SELECT SINCE ID IS DIFFERENT") - - parsedSelection.select() - } - } - - console.log("Parsed: ", parsedSelection.data("id"), selectedNode.data("id")) - }, 2500) - */ - }; + } if (isLoaded && setupSent === false) { @@ -8509,55 +9332,55 @@ const releaseToConnectLabel = "Release to Connect" } const fetchRecommendations = (inputWorkflow) => { - console.log("Disabled recommendations as they were too inaccurate") - return + console.log("Disabled recommendations as they were too inaccurate") + return - const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) + const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) fetch(globalUrl + "/api/v1/workflows/recommend", { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(parsedWorkflow), - credentials: "include", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(parsedWorkflow), + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for usecases"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for usecases"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success !== false) { - //console.log("recommendations: ", responseJson); + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + //console.log("recommendations: ", responseJson); - if (responseJson.actions !== undefined && responseJson.actions !== null) { - console.log("Got recommendations: ", responseJson.actions) + if (responseJson.actions !== undefined && responseJson.actions !== null) { + console.log("Got recommendations: ", responseJson.actions) - //if (cy !== undefined && cy !== null) { - // cy.removeListener("mouseover", "node"); - //} + //if (cy !== undefined && cy !== null) { + // cy.removeListener("mouseover", "node"); + //} - setWorkflowRecommendations(responseJson.actions) + setWorkflowRecommendations(responseJson.actions) - //if (cy !== undefined && cy !== null) { - // cy.on("mouseover", "node", (e) => onNodeHover(e)); - //} - } else { - setWorkflowRecommendations([]) - } - } else { - setWorkflowRecommendations([]) - } - }) - .catch((error) => { - //toast("ERROR: " + error.toString()); - setWorkflowRecommendations([]) - console.log("ERROR getting usecases: " + error.toString()); - }) + //if (cy !== undefined && cy !== null) { + // cy.on("mouseover", "node", (e) => onNodeHover(e)); + //} + } else { + setWorkflowRecommendations([]) + } + } else { + setWorkflowRecommendations([]) + } + }) + .catch((error) => { + //toast("ERROR: " + error.toString()); + setWorkflowRecommendations([]) + console.log("ERROR getting usecases: " + error.toString()); + }) } const fetchUsecases = () => { @@ -8588,81 +9411,111 @@ const releaseToConnectLabel = "Release to Connect" }) } - const getRevisionHistory = (workflow_id) => { - fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions`, { - 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!"); - } - - // Read text from stream - //return response.text(); - return response.json(); - }) - .then((responseJson) => { - if (responseJson === null) { - console.log("No revisions found") - return - } - - if (responseJson.success === false) { - console.log("Error getting workflow revisions: ", responseJson) - return - } - - setAllRevisions(responseJson) - setSelectedVersion(responseJson[0]) - }) - .catch((error) => { - console.log("Error getting workflow revisions: ", error) - }); + const getRevisionHistory = (workflow_id, revisionCount=50, turn=0, orgId="") => { + let headers = { + "Content-Type": "application/json", + Accept: "application/json", } - const loadTriggers = () => { - const url = `${globalUrl}/api/v1/triggers` - fetch(url, - { - method: "GET", - headers: { "content-type": "application/json" }, - credentials: "include", + if (orgId !== "") { + headers["Org-Id"] = orgId + } + + fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions?count=${revisionCount}`, { + method: "GET", + headers: headers, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); } - ) - .then((response) => { - if (response.status !== 200) { - throw new Error("No folders :o!"); - } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success !== false) { - setAllTriggers(responseJson) - } - }) - .catch((error) => { - console.log("Get outlook folders error: ", error.toString()); - }); - } + // Read text from stream + //return response.text(); + return response.json(); + }) + .then((responseJson) => { + if (responseJson === null) { + //console.log("No revisions found") + + //toast.warning("No revisions found") + return + } + + if (responseJson.success === false) { + console.log("Error getting workflow revisions: ", responseJson) + return + } + + setAllRevisions(responseJson) + setSelectedVersion(responseJson[0]) + }) + .catch((error) => { + console.log("Error getting workflow revisions: ", error); + ++turn; + if (turn < 2) { + getRevisionHistory(workflow_id, 5, turn, orgId); + } + }); + } + + const loadTriggers = (orgId) => { + const url = `${globalUrl}/api/v1/triggers` + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id !== "") { + headers["Org-Id"] = workflow.org_id + } + + if (orgId !== undefined && orgId !== null && orgId !== "") { + headers["Org-Id"] = orgId + } + + fetch(url, + { + method: "GET", + headers: headers, + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + throw new Error("No folders :o!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setAllTriggers(responseJson) + } else { + //toast.error("Failed to get triggers") + } + }) + .catch((error) => { + console.log("Get outlook folders error: ", error.toString()); + }); + } // eslint-disable-next-line react-hooks/exhaustive-deps //useEffect(() => { if (firstrequest) { - setFirstrequest(false); - getWorkflow(props.match.params.key, {}); - getChildWorkflows(props.match.params.key) - getRevisionHistory(props.match.params.key) - loadTriggers() + setFirstrequest(false) + getWorkflow(props.match.params.key, {}) + getChildWorkflows(props.match.params.key) + getRevisionHistory(props.match.params.key) + loadTriggers() getApps() fetchUsecases() - setLeftSideBarOpenByClick(false) + getWorkflowExecution(props.match.params.key, "", executionFilter) + + setLeftSideBarOpenByClick(false) localStorage.setItem("expandLeftNav", false) const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; @@ -8705,7 +9558,7 @@ const releaseToConnectLabel = "Release to Connect" console.log("In graph setup") // 2nd load - configures cytoscape - //} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) { + //} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) { } else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined) { //This part has to load LAST, as it's kind of not async. @@ -8741,10 +9594,10 @@ const releaseToConnectLabel = "Release to Connect" if (cy.edgehandles !== undefined) { cy.edgehandles({ handleNodes: (el) => { - // Check of length of el.data() is 1 - if (el.data() === undefined || Object.keys(el.data()).length === 1) { - return false - } + // Check of length of el.data() is 1 + if (el.data() === undefined || Object.keys(el.data()).length === 1) { + return false + } if (el.isNode() && el.data("buttonType") != "ACTIONSUGGESTION" && @@ -8775,9 +9628,18 @@ const releaseToConnectLabel = "Release to Connect" } // preview: true, - cy.fit(null, 400) + try { + cy.fit(null, 400) + } catch (error) { + console.log("Error fitting cytoscape (4): ", error) + } + cy.on("boxselect", "node", (e) => { + e.preventDefault() + e.stopPropagation() + + console.log("BOXSELECT: ", e.boxSelectElements) if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) { e.target.unselect(); } @@ -8787,19 +9649,26 @@ const releaseToConnectLabel = "Release to Connect" cy.on("boxstart", (e) => { console.log("START"); + e.preventDefault() + e.stopPropagation() }); cy.on("boxend", (e) => { - console.log("END: ", cy) + e.preventDefault() + e.stopPropagation() + + console.log("END: ", e.target, cy) var cydata = cy.$(":selected").jsons(); if (cydata !== undefined && cydata !== null && cydata.length > 0) { + // Unselect all nodes + cy.$(":selected").unselect() toast(`Selected ${cydata.length} element(s). CTRL+C to copy them.`); } }); - cy.on('grab', 'edge', (e) => { - console.log("Edge grabbed: ", e.target.data()) - }) + cy.on('grab', 'edge', (e) => { + console.log("Edge grabbed: ", e.target.data()) + }) cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication); @@ -8826,21 +9695,31 @@ const releaseToConnectLabel = "Release to Connect" document.title = "Workflow - " + workflow.name; - startWorkflowStream(props.match.params.key); + startWorkflowStream(props.match.params.key); registerKeys(); } //}) const stopSchedule = (trigger, triggerindex) => { + if (cy !== undefined && cy !== null) { + cy.$(":selected").unselect() + } + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id !== "") { + headers["Org-Id"] = workflow.org_id + } + fetch( `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule/${trigger.id}`, { method: "DELETE", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", } ) @@ -8866,6 +9745,8 @@ const releaseToConnectLabel = "Release to Connect" setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow) + + loadTriggers(workflow.org_id) }) .catch((error) => { console.log("Stop schedule error: ", error.toString()) @@ -8877,7 +9758,7 @@ const releaseToConnectLabel = "Release to Connect" toast("Error: name can't be empty"); return; } - + var mappedStartnode = ""; const alledges = cy.edges().jsons(); if (alledges !== undefined && alledges !== null && alledges.length > 0) { @@ -8906,7 +9787,7 @@ const releaseToConnectLabel = "Release to Connect" if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } - + return response.json(); }) .then((responseJson) => { @@ -8916,21 +9797,22 @@ const releaseToConnectLabel = "Release to Connect" if (data.type === "create") { toast("Pipeline will be created!"); } else if (data.type === "stop") { - toast("Pipeline will be stopped!"); + toast("Pipeline will be stopped!"); } else { toast("Pipeline deleted!") return } - if (trigger.parameters){ - trigger.parameters.push({ - name: data.name, - value: data.command, - });} - + if (trigger.parameters) { + trigger.parameters.push({ + name: data.name, + value: data.command, + }); + } + if (data.type === "stop") trigger.status = "stopped"; else trigger.status = "running"; workflow.triggers[triggerindex] = trigger; - + setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow); @@ -8940,27 +9822,27 @@ const releaseToConnectLabel = "Release to Connect" console.log("Get pipeline error: ", error.toString()); }); }; - + const submitSchedule = (trigger, triggerindex) => { if (trigger.name.length <= 0) { toast("Error: name can't be empty"); return; } - var mappedStartnode = "" - const alledges = cy.edges().jsons() + var mappedStartnode = "" + const alledges = cy.edges().jsons() if (alledges !== undefined && alledges !== null && alledges.length > 0) { - for (let edgekey in alledges) { - const tmp = alledges[edgekey] - if (tmp.data.source === trigger.id) { - mappedStartnode = tmp.data.target - break - } - } - } + for (let edgekey in alledges) { + const tmp = alledges[edgekey] + if (tmp.data.source === trigger.id) { + mappedStartnode = tmp.data.target + break + } + } + } - toast("Creating schedule") - const data = { + toast("Creating schedule") + var data = { name: trigger.name, frequency: workflow.triggers[triggerindex].parameters[0].value, execution_argument: workflow.triggers[triggerindex].parameters[1].value, @@ -8969,14 +9851,32 @@ const releaseToConnectLabel = "Release to Connect" start: mappedStartnode, } + if (data.frequency === undefined || data.frequency === null || data.frequency.length === 0) { + if (isCloud || selectedTrigger?.environment === "cloud") { + data.frequency = "*/25 * * * *" + workflow.triggers[triggerindex].parameters[0].value = "*/25 * * * *" + } else { + data.frequency = "60" + workflow.triggers[triggerindex].parameters[0].value = "60" + } + + setWorkflow(workflow) + } + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id !== "") { + headers["Org-Id"] = workflow.org_id + } + fetch( `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule`, { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, body: JSON.stringify(data), credentials: "include", } @@ -8998,6 +9898,8 @@ const releaseToConnectLabel = "Release to Connect" setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow); + + loadTriggers(workflow.org_id) } }) .catch((error) => { @@ -9008,7 +9910,7 @@ const releaseToConnectLabel = "Release to Connect" const getSigmaInfo = () => { const url = globalUrl + "/api/v1/files/detection/sigma_rules"; - + fetch(url, { method: "GET", credentials: "include", @@ -9022,7 +9924,7 @@ const releaseToConnectLabel = "Release to Connect" toast("Failed to get sigma rules"); } else { setRules(responseJson.sigma_info); - + } }) ) @@ -9032,7 +9934,7 @@ const releaseToConnectLabel = "Release to Connect" }); }; - const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - 57 + const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - 57 const appViewStyle = { marginLeft: 5, marginRight: 5, @@ -9069,7 +9971,7 @@ const releaseToConnectLabel = "Release to Connect" } const VariableItem = (props) => { - const { variable, index, type } = props; + const { variable, index, type } = props; const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -9081,144 +9983,144 @@ const releaseToConnectLabel = "Release to Connect" const deleteVariable = (type, variableIndex) => { - console.log("Delete type: ", type, variableIndex) + console.log("Delete type: ", type, variableIndex) - if (type === "normal") { - if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { - var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) - vars.splice(variableIndex, 1) - workflow.workflow_variables = vars + if (type === "normal") { + if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) + vars.splice(variableIndex, 1) + workflow.workflow_variables = vars - console.log("Workflow after del: ", workflow) + console.log("Workflow after del: ", workflow) - setWorkflow(workflow); - setUpdate(Math.random()); - } - } else if (type === "exec") { - if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { - var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) - vars.splice(variableIndex, 1) - workflow.execution_variables = vars + setWorkflow(workflow); + setUpdate(Math.random()); + } + } else if (type === "exec") { + if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) + vars.splice(variableIndex, 1) + workflow.execution_variables = vars - console.log("Workflow after del: ", workflow) + console.log("Workflow after del: ", workflow) - setWorkflow(workflow); - setUpdate(Math.random()); - } - } + setWorkflow(workflow); + setUpdate(Math.random()); + } + } }; - return ( -
    - { }}> -
    -
    -
    { - setVariableInfo({ - "name": variable.name, - "description": variable.description, - "value": variable.value, - "index": index, - }) + return ( +
    + { }}> +
    +
    +
    { + setVariableInfo({ + "name": variable.name, + "description": variable.description, + "value": variable.value, + "index": index, + }) - if (type === "normal") { - setVariablesModalOpen(true); - } else if (type === "exec") { - setExecutionVariablesModalOpen(true); - } else { - console.log("Unknown type: ", type) - } - }} - > - {variable.name} -
    -
    - - - - { - setOpen(false); - setAnchorEl(null); - }} - > - { - setOpen(false); - setVariableInfo({ - "name": variable.name, - "description": variable.description, - "value": variable.value, - "index": index, - }) + if (type === "normal") { + setVariablesModalOpen(true); + } else if (type === "exec") { + setExecutionVariablesModalOpen(true); + } else { + console.log("Unknown type: ", type) + } + }} + > + {variable.name} +
    +
    + + + + { + setOpen(false); + setAnchorEl(null); + }} + > + { + setOpen(false); + setVariableInfo({ + "name": variable.name, + "description": variable.description, + "value": variable.value, + "index": index, + }) - if (type === "normal") { - setVariablesModalOpen(true); - } else if (type === "exec") { - setExecutionVariablesModalOpen(true); - } else { - console.log("Unknown type: ", type) - } - }} - key={"Edit"} - > - {"Edit"} - - { - deleteVariable(type, index); - setOpen(false); - }} - key={"Delete"} - > - {"Delete"} - - -
    -
    - -
    - ) - } + if (type === "normal") { + setVariablesModalOpen(true); + } else if (type === "exec") { + setExecutionVariablesModalOpen(true); + } else { + console.log("Unknown type: ", type) + } + }} + key={"Edit"} + > + {"Edit"} + + { + deleteVariable(type, index); + setOpen(false); + }} + key={"Delete"} + > + {"Delete"} + + +
    +
    + +
    + ) + } const VariablesView = () => { const variableScrollStyle = { @@ -9246,13 +10148,13 @@ const releaseToConnectLabel = "Release to Connect" ? null : workflow.workflow_variables.map((variable, varindex) => { return ( - + ); - })} + })}
    - + + - const executionArgumentModal = - { }} > - + { + e.preventDefault(); + setExecutionArgumentModalOpen(false) + }} > - { - e.preventDefault(); - setExecutionArgumentModalOpen(false) - }} - > - - - - - Provide an execution argument - - + + + + + Provide an execution argument + + - {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? -
    - {workflow.input_questions.map((question, index) => { + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
    + {workflow.input_questions.map((question, index) => { - return ( -
    - {question.name} - { - var newtext = {} - if (executionText.length > 0) { - try { - newtext = JSON.parse(executionText) - // Check if list or object, then make it object only - if (Array.isArray(newtext)) { - newtext = {} - } - } catch (e) { - console.log("Error parsing JSON: ", e) - } - } + return ( +
    + {question.name} + { + var newtext = {} + if (executionText.length > 0) { + try { + newtext = JSON.parse(executionText) + // Check if list or object, then make it object only + if (Array.isArray(newtext)) { + newtext = {} + } + } catch (e) { + console.log("Error parsing JSON: ", e) + } + } - newtext[question.value] = e.target.value - setExecutionText(JSON.stringify(newtext)) - }} - /> -
    - ) - })} + newtext[question.value] = e.target.value + setExecutionText(JSON.stringify(newtext)) + }} + /> +
    + ) + })} - -
    - : -
    - - At least one node in this workflow requires an execution argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. - + +
    + : +
    + + At least one node in this workflow requires an execution argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. + - - {availableArguments.length > 0 ? -
    - - Previously used arguments: - - {availableArguments.map((data) => { - return ( - { - setExecutionText(data) - executeWorkflow(data, workflow.start, lastSaved); + + {availableArguments.length > 0 ? +
    + + Previously used arguments: + + {availableArguments.map((data) => { + return ( + { + setExecutionText(data) + executeWorkflow(data, workflow.start, lastSaved); - setExecutionArgumentModalOpen(false) - }} - > -
    - - {data} - - - ) - })} -
    - : null} + setExecutionArgumentModalOpen(false) + }} + > +
    + + {data} + + + ) + })} +
    + : null} - -
    - } - + +
    + } +
    const submitQueryModal = () => { - const changeActionTextfield = document.getElementById("change-action-textfield") - if (changeActionTextfield === undefined || changeActionTextfield === null) { - setAiQueryModalOpen(false) - toast.error("Failed to find textfield") - return - } + const changeActionTextfield = document.getElementById("change-action-textfield") + if (changeActionTextfield === undefined || changeActionTextfield === null) { + setAiQueryModalOpen(false) + toast.error("Failed to find textfield") + return + } - if (changeActionTextfield.value === undefined || changeActionTextfield.value === null || changeActionTextfield.value === "") { - toast("Please provide how you want formatting to happen") - return - } + if (changeActionTextfield.value === undefined || changeActionTextfield.value === null || changeActionTextfield.value === "") { + toast("Please provide how you want formatting to happen") + return + } - setAutocompleting(true) - if (codeEditorModalOpen === true) { - autoFormatCodemodal(changeActionTextfield.value) - } else { - aiSubmit(changeActionTextfield.value, undefined, undefined, selectedAction) - } + setAutocompleting(true) + if (codeEditorModalOpen === true) { + autoFormatCodemodal(changeActionTextfield.value) + } else { + aiSubmit(changeActionTextfield.value, undefined, undefined, selectedAction) + } } - const autoFormatCodemodal = (input) => { - if (codeEditorModalOpen !== true) { - toast.error("Code editor is not open") - return - } + const autoFormatCodemodal = (input) => { + if (codeEditorModalOpen !== true) { + toast.error("Code editor is not open") + return + } - if (editorData.name === undefined || editorData.name === null || editorData.name === "") { - toast.error("Failed to find editor field name") - return - } + if (editorData.name === undefined || editorData.name === null || editorData.name === "") { + toast.error("Failed to find editor field name") + return + } - const codeeditor = document.getElementById("shuffle-codeeditor") - if (codeeditor === undefined || codeeditor === null) { - toast.error("Failed to find code editor html") - return - } + const codeeditor = document.getElementById("shuffle-codeeditor") + if (codeeditor === undefined || codeeditor === null) { + toast.error("Failed to find code editor html") + return + } - const editorInstance = window?.ace?.edit("shuffle-codeeditor") - if (editorInstance === undefined || editorInstance === null) { - toast.error("Failed to find code editor instance") - return - } + const editorInstance = window?.ace?.edit("shuffle-codeeditor") + if (editorInstance === undefined || editorInstance === null) { + toast.error("Failed to find code editor instance") + return + } - //console.log("ACE data: ", editorInstance.getValue()) - //editorInstance.setValue("HELO") + //console.log("ACE data: ", editorInstance.getValue()) + //editorInstance.setValue("HELO") - // Should try to automatically fix this input - console.log("Running AI input fixer: ", selectedResult) - if (aiSubmit === undefined || selectedAction === undefined) { - toast.error("Failed to find AI submit function") - return - } - - // Should remove params from selectedAction that aren't parameterName - var tmpAction = JSON.parse(JSON.stringify(selectedAction)) - var tmpParams = tmpAction.parameters.filter((param) => param.name === editorData.name) - if (tmpParams.length !== 1) { - toast.error("Failed to find correct parameter in action") - return - } + // Should try to automatically fix this input + console.log("Running AI input fixer: ", selectedResult) + if (aiSubmit === undefined || selectedAction === undefined) { + toast.error("Failed to find AI submit function") + return + } - tmpParams[0].value = editorInstance.getValue() - tmpAction.parameters = tmpParams - aiSubmit(input, undefined, undefined, tmpAction) - } + // Should remove params from selectedAction that aren't parameterName + var tmpAction = JSON.parse(JSON.stringify(selectedAction)) + var tmpParams = tmpAction.parameters.filter((param) => param.name === editorData.name) + if (tmpParams.length !== 1) { + toast.error("Failed to find correct parameter in action") + return + } - const aiQueryModal = + tmpParams[0].value = editorInstance.getValue() + tmpAction.parameters = tmpParams + aiSubmit(input, undefined, undefined, tmpAction) + } + + const aiQueryModal = { - setAiQueryModalOpen(false) + setAiQueryModalOpen(false) }} > - + { + }} > - { - }} - > - - - - { - setAiQueryModalOpen(false) - }} - > - - - - Shuffle AI - - - What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Alpha feature. Please give feedback to support@shuffler.io {"<"}3 + + + + { + setAiQueryModalOpen(false) + }} + > + + + + Shuffle AI + + + What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Beta feature. Please give feedback to support@shuffler.io {"<"}3 - - - - - ), - onKeyPress: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - submitQueryModal() - } - }, - }} + + + + + ), + onKeyPress: (e) => { + if (e.key === "Enter" && !e.shiftKey) { + submitQueryModal() + } + }, + }} - /> + /> + const handleConditionFieldChange = (fieldType, fieldName, value) => { + if (fieldType === "source") { + setSourceValue({ + ...sourceValue, + value: value + }); + } else if (fieldType === "destination") { + setDestinationValue({ + ...destinationValue, + value: value + }); + } + }; + const conditionsModal = ( { @@ -12192,7 +13541,7 @@ const releaseToConnectLabel = "Release to Connect" bottom: 10, left: 10, color: "rgba(255,255,255,0.6)", - zIndex: 10000, + zIndex: 10000, }} > Conditions can't be used for loops [ .# ]{" "} @@ -12200,10 +13549,10 @@ const releaseToConnectLabel = "Release to Connect" rel="noopener noreferrer" target="_blank" href="/docs/workflows#conditions" - style={{ - textDecoration: "none", - color: "#FF8544", - }} + style={{ + textDecoration: "none", + color: "#FF8544", + }} > Learn more @@ -12256,6 +13605,9 @@ const releaseToConnectLabel = "Release to Connect" tmpdata={sourceValue} setData={setSourceValue} type={"source"} + setExpansionModalOpen={setCodeEditorModalOpen} + setActiveDialog={setActiveDialog} + setEditorData={setEditorData} />
    @@ -12420,6 +13775,7 @@ const releaseToConnectLabel = "Release to Connect" variant="text" onClick={() => { setConditionsModalOpen(false); + setCodeEditorModalOpen(false) setSourceValue({}); setConditionValue({}); setDestinationValue({}); @@ -12441,6 +13797,7 @@ const releaseToConnectLabel = "Release to Connect" }; setConditionsModalOpen(false); + setCodeEditorModalOpen(false) if (selectedEdge.conditions === undefined || selectedEdge.conditions === null) { selectedEdge.conditions = [data]; } else { @@ -12525,7 +13882,6 @@ const releaseToConnectLabel = "Release to Connect" }; const menuClick = (event) => { - console.log("MENU CLICK"); setOpen(!open); setAnchorEl(event.currentTarget); }; @@ -12555,6 +13911,7 @@ const releaseToConnectLabel = "Release to Connect" setConditionValue(condition.condition); setDestinationValue(condition.destination); setConditionsModalOpen(true); + setCodeEditorModalOpen(false) }} >
    -
    +
    +

    - Conditions + Conditions

    - - What are conditions? - +
    + {selectedEdge?.source && workflow?.actions ? + Source + : null + } + + {/* Add arrow icon */} + { + selectedEdge && Object.keys(selectedEdge).length > 0 ? + + : null + } + + {/* Destination node image */} + {selectedEdge?.target && workflow?.actions ? + Destination + : null + } +
    + + What are conditions? + +
    - {/* Check if dest is the same as start */} - {conditionsDisabled ? - - Conditions are unavailable between triggers and the startnode. - - : null} + {/* Check if dest is the same as start */} + {conditionsDisabled ? + + Conditions are unavailable between triggers and the startnode. + + : null} -
    - - - {/* + {/* -
    + +
    ); }; - const handleWorkflowSelectionUpdate = (e, isUserinput) => { + 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. Value: ", e.target.value); - return null - } + if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { + console.log("Returning as there's no id. Value: ", e.target.value); + return null + } - const paramIndex = isUserinput === true ? 5 : 0 + const paramIndex = isUserinput === true ? 5 : 0 - console.log("USERINPUT: ", paramIndex, workflow.triggers[selectedTriggerIndex]) - if (workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === undefined || workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === null) { - workflow.triggers[selectedTriggerIndex].parameters[paramIndex] = { - "name": "subflow", - "value": "", - } - } + console.log("USERINPUT: ", paramIndex, workflow.triggers[selectedTriggerIndex]) + if (workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === undefined || workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === null) { + workflow.triggers[selectedTriggerIndex].parameters[paramIndex] = { + "name": "subflow", + "value": "", + } + } - setUpdate(Math.random()); - workflow.triggers[selectedTriggerIndex].parameters[paramIndex].value = e.target.value.id; - setSubworkflow(e.target.value); + setUpdate(Math.random()); + workflow.triggers[selectedTriggerIndex].parameters[paramIndex].value = e.target.value.id; + setSubworkflow(e.target.value); - // Sets the startnode - if (e.target.value.id !== workflow.id && e.target.value.id.length > 0 ) { + // Sets the startnode + if (e.target.value.id !== workflow.id && e.target.value.id.length > 0) { - const startnode = e?.target?.value?.actions?.find((action) => action.id === e.target.value.start); - + const startnode = e?.target?.value?.actions?.find((action) => action.id === e.target.value.start); - if (startnode !== undefined && startnode !== null) { - setSubworkflowStartnode(startnode); - if (paramIndex === 0) { - try { - workflow.triggers[selectedTriggerIndex].parameters[3].value = startnode.id; - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "startnode", - value: startnode.id, - }; - } - } + if (startnode !== undefined && startnode !== null) { + setSubworkflowStartnode(startnode); - //setWorkflow(workflow); - } - } else { - console.log("WORKFLOW: ", workflow); - } + if (paramIndex === 0) { + try { + workflow.triggers[selectedTriggerIndex].parameters[3].value = startnode.id; + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "startnode", + value: startnode.id, + }; + } + } - setWorkflow(workflow); - } + //setWorkflow(workflow); + } + } else { + console.log("WORKFLOW: ", workflow); + } + + setWorkflow(workflow); + } // Function to transform the data const transformAuthData = (authData) => { const transformedData = {}; @@ -12919,7 +14327,7 @@ const releaseToConnectLabel = "Release to Connect" }) appIdsInWorkflow = [...new Set(appIdsInWorkflow)]; - + // loop through the authData and create transformedData which looks like: // appId: [auth1, auth2, ...] authData.forEach((auth) => { @@ -12938,7 +14346,7 @@ const releaseToConnectLabel = "Release to Connect" }); return transformedData; - + }; const AppAuthSelector = ({ appAuthData }) => { @@ -12963,7 +14371,7 @@ const releaseToConnectLabel = "Release to Connect" if (mappingWithName[appName] !== undefined) { return mappingWithName[appName]; } - + return "no-overrides"; } @@ -12972,10 +14380,10 @@ const releaseToConnectLabel = "Release to Connect" if (authId === "no-override") { // remove the override parameter - let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; + let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; // replace from appName= to the next ; let newValue = oldValue.replace(new RegExp(appName + "=[^;]*;"), ""); - + workflow.triggers[selectedTriggerIndex].parameters[5].value = newValue setSelectedAuth(""); return @@ -12998,19 +14406,19 @@ const releaseToConnectLabel = "Release to Connect" // return; // } // } - - if (workflow.triggers[selectedTriggerIndex].parameters[5] === undefined || workflow.triggers[selectedTriggerIndex].parameters[5] === null) { - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "auth_override", - value: "", - }; - } - - let authGroupValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; - if (authGroupValue === undefined || authGroupValue === null || authGroupValue === "") { + if (workflow.triggers[selectedTriggerIndex].parameters[5] === undefined || workflow.triggers[selectedTriggerIndex].parameters[5] === null) { + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "auth_override", + value: "", + }; + } + + let authGroupValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; + + if (authGroupValue === undefined || authGroupValue === null || authGroupValue === "") { workflow.triggers[selectedTriggerIndex].parameters[5].value = appName + "=" + auth.id + ";"; - } else { + } else { // check if the app is already in the list if (authGroupValue.includes(appName)) { let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; @@ -13019,7 +14427,7 @@ const releaseToConnectLabel = "Release to Connect" } else { workflow.triggers[selectedTriggerIndex].parameters[5].value += appName + "=" + auth.id + ";"; } - } + } // workflow.triggers[selectedTriggerIndex].parameters.push({ // name: auth.label + "_" + auth.app.id + "_override", @@ -13029,780 +14437,587 @@ const releaseToConnectLabel = "Release to Connect" } return ( -
    - {Object.entries(transformedAuthData).map(([appId, authList]) => { - if (authList === undefined || authList === null || authList.length < 2) { - return null; - } +
    + {Object.entries(transformedAuthData).map(([appId, authList]) => { + if (authList === undefined || authList === null || authList.length < 2) { + return null; + } - return ( -
    - - handleSelectChange(authList[0].app.name, authList[0].app.id, e)} + className="auth-select" style={{ + width: '100%', + padding: '10px', + fontSize: '16px', + borderRadius: '4px', + border: '1px solid #555', backgroundColor: theme.palette.inputColor, - fontSize: "1.2em", + color: '#E8E8E8', + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)', + transition: 'border-color 0.2s, box-shadow 0.2s', }} + onFocus={(e) => e.target.style.borderColor = '#007BFF'} + onBlur={(e) => e.target.style.borderColor = '#555'} > - {auth.label} - - )} - -
    - )})} -
    + + {authList.flatMap((auth) => + + )} + +
    + ) + })} +
    ); }; - const SubflowSidebar = () => { - const [menuPosition, setMenuPosition] = useState(null); - const [showDropdown, setShowDropdown] = React.useState(false); - const [actionlist, setActionlist] = React.useState([]); - if (actionlist.length === 0) { - // FIXME: Have previous execution values in here - actionlist.push({ - type: "Execution Argument", - name: "Execution Argument", - value: "$exec", - highlight: "exec", - autocomplete: "exec", - example: "hello", - }) - actionlist.push({ - type: "Shuffle Database", - name: "Shuffle Database", - value: "$shuffle_cache", - highlight: "shuffle_db", - autocomplete: "shuffle_cache", - example: "hello", - }) - if ( - workflow.workflow_variables !== null && - workflow.workflow_variables !== undefined && - workflow.workflow_variables.length > 0 - ) { - for (let varkey in workflow.workflow_variables) { - const item = workflow.workflow_variables[varkey]; - actionlist.push({ - type: "workflow_variable", - name: item.name, - value: item.value, - id: item.id, - autocomplete: `${item.name.split(" ").join("_")}`, - example: item.value, - }); - } + const iconStyle = { + marginRight: 15, + }; + + + if (selectedTrigger !== undefined && selectedTrigger !== null && Object.getOwnPropertyNames(selectedTrigger)?.length > 0) { + if (workflow.triggers[selectedTriggerIndex] === undefined) { + return null; + } + + if ( + workflow.triggers[selectedTriggerIndex].parameters === undefined || + workflow.triggers[selectedTriggerIndex].parameters === null || + workflow.triggers[selectedTriggerIndex].parameters.length === 0 + ) { + workflow.triggers[selectedTriggerIndex].parameters = []; + workflow.triggers[selectedTriggerIndex].parameters[0] = { + name: "workflow", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "argument", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[2] = { + name: "user_apikey", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "startnode", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "check_result", + value: "false", + }; + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "auth_override", + value: "", + }; + + } + + var handleSubflowStartnodeSelection = (e) => { + setSubworkflowStartnode(e.target.value); + + if (e.target.value === null || e.target.value === undefined) { + return } - // FIXME: Add values from previous executions if they exist - if ( - workflow.execution_variables !== null && - workflow.execution_variables !== undefined && - workflow.execution_variables.length > 0 - ) { - for (let varkey in workflow.execution_variables) { - const item = workflow.execution_variables[varkey]; - actionlist.push({ - type: "execution_variable", - name: item.name, - value: item.value, - id: item.id, - autocomplete: `${item.name.split(" ").join("_")}`, - example: "", - }); - } - } + const branchId = uuidv4(); + const newbranch = { + source_id: workflow.triggers[selectedTriggerIndex].id, + destination_id: e.target.value.id, + source: workflow.triggers[selectedTriggerIndex].id, + target: e.target.value.id, + has_errors: false, + id: branchId, + _id: branchId, + label: "Subflow", + decorator: true, + }; - var parents = getParents(selectedTrigger); - if (parents.length > 1) { - for (let parentkey in parents) { - const item = parents[parentkey]; - if (item.label === "Execution Argument") { - continue; - } + if (workflow.visual_branches !== undefined) { + if (workflow.visual_branches === null) { + workflow.visual_branches = [newbranch]; + } else if (workflow.visual_branches.length === 0) { + workflow.visual_branches.push(newbranch); + } else { + const foundIndex = workflow.visual_branches.findIndex( + (branch) => branch.source_id === newbranch.source_id + ); - var exampledata = item.example === undefined ? "" : item.example; - // Find previous execution and their variables - if (workflowExecutions.length > 0) { - // Look for the ID - for (let execkey in workflowExecutions) { - if ( - workflowExecutions[execkey].results === undefined || - workflowExecutions[execkey].results === null - ) { - continue; - } - - var foundResult = workflowExecutions[execkey].results.find( - (result) => result.action.id === item.id - ); - if (foundResult === undefined) { - continue; - } - - const validated = validateJson(foundResult.result) - if (validated.valid) { - exampledata = validateJson.result - break - } + if (foundIndex !== -1) { + const currentEdge = cy.getElementById( + workflow.visual_branches[foundIndex].id + ); + if ( + currentEdge !== undefined && + currentEdge !== null + ) { + currentEdge.remove(); } } - // 1. Take - const actionvalue = { - type: "action", - id: item.id, - name: item.label, - autocomplete: `${item?.label?.split(" ")?.join("_")}`, - example: exampledata, - } - actionlist.push(actionvalue); + workflow.visual_branches.splice(foundIndex, 1); + workflow.visual_branches.push(newbranch); } } - setActionlist(actionlist); + if (workflow.id === subworkflow.id) { + const cybranch = { + group: "edges", + source: newbranch.source_id, + target: newbranch.destination_id, + id: branchId, + data: newbranch, + }; + + cy.add(cybranch); + } + + console.log("Value to be set: ", e.target.value); + try { + workflow.triggers[ + selectedTriggerIndex + ].parameters[3].value = e.target.value.id; + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = + { + name: "startnode", + value: e.target.value.id, + }; + } + + setWorkflow(workflow); + } + } + + const handleMenuClose = () => { + setUpdate(Math.random()); + setMenuPosition(null); + }; + + const handleItemClick = (values) => { + console.log("VALUES: ", values) + if (values === undefined || values === null || values.length === 0) { + return; } - const handleMenuClose = () => { - setUpdate(Math.random()); - setMenuPosition(null); - }; + /* + workflow.triggers[selectedTriggerIndex].parameters[1].value + .trim() + .endsWith("$") + ? values[0].autocomplete + : "$" + values[0].autocomplete; - const handleItemClick = (values) => { - console.log("VALUES: ", values) - if (values === undefined || values === null || values.length === 0) { - return; - } - - - /* - workflow.triggers[selectedTriggerIndex].parameters[1].value - .trim() - .endsWith("$") - ? values[0].autocomplete - : "$" + values[0].autocomplete; - - for (var key in values) { - if (key === 0 || values[key].autocomplete.length === 0) { - continue; - } - - toComplete += values[key].autocomplete - } - */ - - console.log("SELECTED TRIGGER: ", selectedTrigger) - if (selectedTrigger.name === "Shuffle Workflow") { - const toComplete = selectedTrigger.parameters[1].value + "$" + values[0].autocomplete - selectedTrigger.parameters[1].value = toComplete - setSelectedTrigger(selectedTrigger) - } - - setUpdate(Math.random()); - setShowDropdown(false); - setMenuPosition(null); - }; - - const iconStyle = { - marginRight: 15, - }; - - - if (Object.getOwnPropertyNames(selectedTrigger)?.length > 0) { - if (workflow.triggers[selectedTriggerIndex] === undefined) { - return null; + for (var key in values) { + if (key === 0 || values[key].autocomplete.length === 0) { + continue; } - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow.triggers[selectedTriggerIndex].parameters.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "workflow", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "argument", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "user_apikey", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "startnode", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "check_result", - value: "false", - }; - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "auth_override", - value: "", - }; + toComplete += values[key].autocomplete + } + */ - /* - // API-key has been replaced by auth key for the execution. - // Parents can now automatically execute children without auth from a user, as long as the subflow in question is owned by the same org and the subflow is actually referencing it during checkin. - console.log("SETTINGS: ", userSettings); - if ( - userSettings !== undefined && - userSettings !== null && - userSettings.apikey !== null && - userSettings.apikey !== undefined && - userSettings.apikey.length > 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "user_apikey", - value: userSettings.apikey, - }; - } - */ + if (selectedTrigger.name === "Shuffle Workflow") { + const toComplete = workflow?.triggers?.[selectedTriggerIndex]?.parameters?.[1]?.value + "$" + values[0]?.autocomplete + // selectedTrigger.parameters[1].value = toComplete + workflow.triggers[selectedTriggerIndex].parameters[1].value = toComplete + const foundfield = document.getElementById("subflow_exec_field") + if (foundfield !== undefined && foundfield !== null) { + foundfield.value = toComplete } + setWorkflow(workflow) + } - const handleSubflowStartnodeSelection = (e) => { - setSubworkflowStartnode(e.target.value); + setUpdate(Math.random()); + setShowDropdown(false); + setMenuPosition(null); + }; - if (e.target.value === null || e.target.value === undefined) { - return + const subflowtypes = [ + { + name: "Any", + }, + { + name: "Enrich", + }, + { + name: "Ticket Creation", + } + ] + + const handleSubflowParamChange = (triggerId, triggerField, newData) => { + + if (workflow !== undefined && workflow !== null) { + // Find the trigger with matching id + const triggerIndex = workflow?.triggers?.findIndex(trigger => trigger.id === triggerId); + if (triggerIndex >= 0) { + // Find the parameter with matching name + const paramIndex = workflow.triggers[triggerIndex].parameters?.findIndex(param => param.name === triggerField); + if (paramIndex >= 0) { + // Update the parameter value + workflow.triggers[triggerIndex].parameters[paramIndex].value = newData; + + // Update workflow state to trigger re-render + setWorkflow({...workflow}); + setSelectedTriggerValue(newData) + setLastSaved(false); } - - const branchId = uuidv4(); - const newbranch = { - source_id: workflow.triggers[selectedTriggerIndex].id, - destination_id: e.target.value.id, - source: workflow.triggers[selectedTriggerIndex].id, - target: e.target.value.id, - has_errors: false, - id: branchId, - _id: branchId, - label: "Subflow", - decorator: true, - }; - - if (workflow.visual_branches !== undefined) { - if (workflow.visual_branches === null) { - workflow.visual_branches = [newbranch]; - } else if (workflow.visual_branches.length === 0) { - workflow.visual_branches.push(newbranch); - } else { - const foundIndex = workflow.visual_branches.findIndex( - (branch) => branch.source_id === newbranch.source_id - ); - if (foundIndex !== -1) { - const currentEdge = cy.getElementById( - workflow.visual_branches[foundIndex].id - ); - if ( - currentEdge !== undefined && - currentEdge !== null - ) { - currentEdge.remove(); - } - } - - workflow.visual_branches.splice(foundIndex, 1); - workflow.visual_branches.push(newbranch); - } - } - - if (workflow.id === subworkflow.id) { - const cybranch = { - group: "edges", - source: newbranch.source_id, - target: newbranch.destination_id, - id: branchId, - data: newbranch, - }; - - cy.add(cybranch); - } - - console.log("Value to be set: ", e.target.value); - try { - workflow.triggers[ - selectedTriggerIndex - ].parameters[3].value = e.target.value.id; - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = - { - name: "startnode", - value: e.target.value.id, - }; - } - - setWorkflow(workflow); } + } + } - - const subflowtypes = [ - { - name: "Any", - }, - { - name: "Enrich", - } - ] - - return ( -
    - -

    - {selectedTrigger.app_name} -

    - - - -
    - - What are subflows? - - + +

    + {selectedTrigger.app_name} +

    + + + +
    + + What are subflows? + + +
    +
    + Name + 0} + /> +
    +
    +
    + + + Delay + { - if (data.id === workflow.id) { - data = workflow; + InputProps={{ + style: { + color: "white" + } + }} + size="small" + placeholder={selectedTrigger.execution_delay} + defaultValue={selectedTrigger?.execution_delay || 0} + onChange={(event) => { + if (isNaN(event.target.value)) { + console.log("NAN: ", event.target.value) + return } - //key={index} - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose Subflow '{data.name}' - - - }> - { - getWorkflowApps(data.id); - handleWorkflowSelectionUpdate({ - target: { - value: data - } - }) - }} - > - - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - - ); + const parsedNumber = parseInt(event.target.value) + if (parsedNumber > 86400) { + console.log("Max number is 1 day (86400)") + return + } + + selectedTrigger.execution_delay = parseInt(event.target.value) + setSelectedTrigger(selectedTrigger) }} /> - )} + + +
    +
    +
    + { + const newvalue = workflow.triggers[selectedTriggerIndex].parameters[4] === undefined || workflow.triggers[selectedTriggerIndex].parameters[4].value === "false" ? "true" : "false"; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "check_result", + value: newvalue, + }; - {subworkflow === undefined || - subworkflow === null || - subworkflow.id === undefined || - subworkflow.actions === null || - subworkflow.actions === undefined || - subworkflow.actions.length === 0 ? null : ( - -
    -
    - Select the Startnode -
    -
    - option.id === value.id} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.label === undefined || option.label === null) { - if (option.length === 36) { + setWorkflow(workflow); + setUpdate(Math.random()); + }} + color="primary" + value="Wait for results" + /> + } + style={{ marginTop: 10 }} + label={
    Wait for results
    } + /> +
    +
    +
    +
    +
    + Select a workflow to execute +
    +
    + {workflow.triggers[selectedTriggerIndex].parameters[0].value + .length === 0 ? null : workflow.triggers[selectedTriggerIndex] + .parameters[0].value === props.match.params.key ? null : ( +
    + + + +
    + )} +
    - } + {workflows === undefined || + workflows === null || + workflows.length === 0 ? null : ( - return "Default"; + option.id === value.id} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + setLastSaved(false) + 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": [], } + } + } + } - const newname = ( - option.label.charAt(0).toUpperCase() + option.label.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={subworkflow.actions} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette?.borderRadius, - }} - onChange={(event, newValue) => { - setLastSaved(false) - handleSubflowStartnodeSelection({ target: { value: newValue } }) - }} - renderOption={(props, action, state) => { - const isParent = getParents(selectedTrigger).find( - (parent) => parent.id === action.id - ) + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } - return ( - { - if (subworkflow.id === workflow.id) { - handleActionHover(true, action.id) - } - }} - onMouseOut={() => { - if (subworkflow.id === workflow.id) { - handleActionHover(false, action.id) - } - }} - disabled={isCloud && isParent} - onClick={() => { - handleSubflowStartnodeSelection({ - target: { - value: action - } - }) - }} - style={{ - backgroundColor: theme.palette.inputColor, - color: isParent ? "red" : "white", - }} - value={action} - > - {action.label} - - ); - }} - renderInput={(params) => { - return ( - - ); - }} + //key={index} + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose Subflow '{data.name}' + + + }> + { + getWorkflowApps(data.id); + handleWorkflowSelectionUpdate({ + target: { + value: data + } + }) + document.activeElement.blur(); + }} + > + + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + - - )} + ); + }} + /> + )} + + {subworkflow === undefined || + subworkflow === null || + subworkflow.id === undefined || + subworkflow.actions === null || + subworkflow.actions === undefined || + subworkflow.actions.length === 0 ? null : ( +
    - Execution Argument + Select the Startnode
    - - - { - setMenuPosition({ - top: event.pageY + 10, - left: event.pageX + 10, - }); - //setShowDropdownNumber(3) - setShowDropdown(true); - }} - /> - - - ), }} - rows="6" - multiline - fullWidth - color="primary" - placeholder="Some execution data" - defaultValue={ - workflow.triggers[selectedTriggerIndex].parameters[1].value - } - onBlur={(e) => { - setLastSaved(false) + sx={{ + '& .MuiOutlinedInput-root': { + height: 40, // Adjust the input height + }, + '& .MuiAutocomplete-input': { + padding: '8px', // Adjust the text padding + }, + }} + getOptionSelected={(option, value) => option.id === value.id} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.label === undefined || option.label === null) { + if (option.length === 36) { - workflow.triggers[selectedTriggerIndex].parameters[1].value = e.target.value - setWorkflow(workflow) + } + + return "Default"; + } + + const newname = ( + option.label.charAt(0).toUpperCase() + option.label.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={subworkflow.actions} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + setLastSaved(false) + handleSubflowStartnodeSelection({ target: { value: newValue } }) + }} + renderOption={(props, action, state) => { + const isParent = getParents(selectedTrigger).find( + (parent) => parent.id === action.id + ) + + return ( + { + if (subworkflow.id === workflow.id) { + handleActionHover(true, action.id) + } + }} + onMouseOut={() => { + if (subworkflow.id === workflow.id) { + handleActionHover(false, action.id) + } + }} + disabled={isCloud && isParent} + onClick={() => { + handleSubflowStartnodeSelection({ + target: { + value: action + } + }) + document.activeElement.blur() + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: isParent ? "red" : "white", + }} + value={action} + > + {action.label} + + ); + }} + renderInput={(params) => { + return ( + + ); }} /> - {!showDropdown ? null : - { - handleMenuClose(); - }} - open={!!menuPosition} - style={{ - border: `2px solid #FF8544`, - color: "white", - marginTop: 2, - }} - > - {actionlist.map((innerdata) => { - const icon = - innerdata.type === "action" ? ( - - ) : innerdata.type === "workflow_variable" || - innerdata.type === "execution_variable" ? ( - - ) : ( - - ); - - const handleExecArgumentHover = (inside) => { - var exec_text_field = document.getElementById( - "execution_argument_input_field" - ); - if (exec_text_field !== null) { - if (inside) { - exec_text_field.style.border = "2px solid #FF8544"; - } else { - exec_text_field.style.border = ""; - } + + )} +
    +
    + Runtime Argument + + {/*parentParamValue !== undefined && parentParamValue !== null && parentParamValue !== "" && parentParamValue !== data.value ? + + { + selectedActionParameters[count].value = parentParamValue + selectedAction.parameters[count].value = parentParamValue + setSelectedAction(selectedAction) + setUpdate(Math.random()) + }} + /> + + : null*/} + + + { + event.preventDefault() + setCodeEditorModalOpen(true) + setActiveDialog("codeeditor") + var parsedvalue = workflow?.triggers[selectedTriggerIndex]?.parameters[1]?.value + + // Right now Handling subflow only with this + navigate(`?trigger_id=${selectedTrigger.id}&trigger_field=${"argument"}&trigger_name=${selectedTrigger.label}`) + setEditorData({ + "name": workflow.triggers[selectedTriggerIndex].parameters[1].name, + "value": parsedvalue, + "field_number": 1, + "actionlist": subflowActionList, + "field_id": "subflow_exec_field", + }) + }} + > + + + +
    +
    + + + { + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + //setShowDropdownNumber(3) + setShowDropdown(true); + }} + /> + + + ), + }} + rows="6" + multiline + fullWidth + color="primary" + placeholder="Some execution data" + value={ + selectedTriggerValue + } + onChange={(e) => { + setLastSaved(false) + setSelectedTriggerValue(e.target.value) + }} + onBlur={() => { + workflow.triggers[selectedTriggerIndex].parameters[1].value = selectedTriggerValue + setWorkflow(workflow) + }} + /> + {!showDropdown ? null : + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + border: `2px solid #FF8544`, + color: "white", + marginTop: 2, + }} + > + {subflowActionList.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : ( + + ); + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #FF8544"; + } else { + exec_text_field.style.border = ""; } - - // Also doing arguments - if ( - workflow.triggers !== undefined && - workflow.triggers !== null && - workflow.triggers.length > 0 - ) { - for (let triggerkey in workflow.triggers) { - const item = workflow.triggers[triggerkey]; - - if (cy !== undefined) { - var node = cy.getElementById(item.id); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } + } + + // Also doing arguments + if ( + workflow.triggers !== undefined && + workflow.triggers !== null && + workflow.triggers.length > 0 + ) { + for (let triggerkey in workflow.triggers) { + const item = workflow.triggers[triggerkey]; + + if (cy !== undefined && cy !== null) { + var node = cy.getElementById(item.id); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); } } } } } - - const handleActionHover = (inside, actionId) => { - if (cy !== undefined) { - var node = cy.getElementById(actionId); - if (node.length > 0) { - if (inside) { - node.addClass("shuffle-hover-highlight"); - } else { - node.removeClass("shuffle-hover-highlight"); - } + } + + const handleActionHover = (inside, actionId) => { + if (cy !== undefined && cy !== null) { + var node = cy.getElementById(actionId); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); } } - }; - - const handleMouseover = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(true); - } else if (innerdata.type === "action") { - handleActionHover(true, innerdata.id); - } - }; - - const handleMouseOut = () => { - if (innerdata.type === "Execution Argument") { - handleExecArgumentHover(false); - } else if (innerdata.type === "action") { - handleActionHover(false, innerdata.id); - } - }; - - var parsedPaths = []; - console.log("Found example data: ", innerdata.example) - if (typeof innerdata.example === "object") { - parsedPaths = GetParsedPaths(innerdata.example, ""); } - - const coverColor = "#82ccc3" - - return parsedPaths.length > 0 ? ( - + }; + + const handleMouseover = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + + return parsedPaths.length > 0 ? ( + {/* */} - + { - console.log("CLICKED: ", innerdata); console.log(innerdata.example) handleItemClick([innerdata]); }} > - - + + - + {innerdata.name} - + {parsedPaths.map((pathdata, index) => { // FIXME: Should be recursive in here // const icon = pathdata.type === "value" ? ( - + ) : pathdata.type === "list" ? ( - + ) : ( - + ); // - - const indentation_count = (pathdata.name.match(/\./g) || []).length+1 - const baseIndent =
    + + const indentation_count = (pathdata.name.match(/\./g) || []).length + 1 + const baseIndent =
    //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 - const boxPadding = 0 + const boxPadding = 0 const namesplit = pathdata.name.split(".") - const newname = namesplit[namesplit.length-1] + const newname = namesplit[namesplit.length - 1] return ( { @@ -14104,9 +15489,9 @@ const releaseToConnectLabel = "Release to Connect" baseIndent ) })} - {icon} {newname} - {pathdata.type === "list" ? { - + {icon} {newname} + {pathdata.type === "list" ? { + }} /> : null}
    @@ -14115,38 +15500,38 @@ const releaseToConnectLabel = "Release to Connect" })} - - ) : ( - handleMouseover()} - onMouseOut={() => { - handleMouseOut(); - }} - onClick={() => { - handleItemClick([innerdata]); - }} + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + - -
    - {icon} {innerdata.name} -
    -
    -
    - ); - })} -
    - } - {/* +
    + {icon} {innerdata.name} +
    + + + ); + })} +
    + } + {/*
    */} -
    -
    +
    +
    - {/* + {/*
    @@ -14202,51 +15587,23 @@ const releaseToConnectLabel = "Release to Connect"
    */} -
    - ); - } +
    - return null; - }; const CommentSidebar = () => { if (Object.getOwnPropertyNames(selectedComment).length > 0) { - /* - if (workflow.triggers[selectedTriggerIndex] === undefined) { - return null - } - - if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) { - workflow.triggers[selectedTriggerIndex].parameters = [] - workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "url", "value": referenceUrl+"webhook_"+selectedTrigger.id} - workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "tmp", "value": "webhook_"+selectedTrigger.id} - workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "auth_headers", "value": ""} - setWorkflow(workflow) - } else { - if (selectedTrigger.environment !== "cloud") { - const newUrl = referenceUrl+"webhook_"+selectedTrigger.id - if (newUrl !== workflow.triggers[selectedTriggerIndex].parameters[0].value) { - console.log("Url is wrong - should update. This functionality is temporarily disabled.") - //workflow.triggers[selectedTriggerIndex].parameters[0].value = newUrl - //setWorkflow(workflow) - } - } - } - - const trigger_header_auth = workflow.triggers[selectedTriggerIndex].parameters.length > 2 ? workflow.triggers[selectedTriggerIndex].parameters[2].value : "" - */ return (
    -

    Comment

    - - What are comments? - +

    Comment

    + + What are comments? + 0 && workflow?.triggers !== null && workflow?.triggers !== undefined && workflow?.triggers?.length >= selectedTriggerIndex && workflow?.triggers[selectedTriggerIndex] !== undefined ) { - if (selectedTrigger.trigger_type === "SCHEDULE" && workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null) { - console.log("Autofixing schedule") + if (Object.getOwnPropertyNames(selectedTrigger)?.length > 0 && workflow?.triggers !== null && workflow?.triggers !== undefined && workflow?.triggers?.length >= selectedTriggerIndex && workflow?.triggers[selectedTriggerIndex] !== undefined) { + if (selectedTrigger.trigger_type === "SCHEDULE" && workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null) { + console.log("Autofixing schedule") + workflow.triggers[selectedTriggerIndex].parameters = []; + workflow.triggers[selectedTriggerIndex].parameters[0] = { + name: "cron", + value: isCloud ? "*/25 * * * *" : "60", + }; + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "execution_argument", + value: '{"name": "value"}', + }; + setWorkflow(workflow); + } else if (selectedTrigger.trigger_type === "WEBHOOK") { + if (workflow.triggers[selectedTriggerIndex] === undefined) { + return null; + } + + if ( + workflow.triggers[selectedTriggerIndex].parameters === undefined || + workflow.triggers[selectedTriggerIndex].parameters === null || + workflow.triggers[selectedTriggerIndex].parameters.length === 0 + ) { workflow.triggers[selectedTriggerIndex].parameters = []; workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "cron", - value: isCloud ? "*/25 * * * *" : "60", + name: "url", + value: referenceUrl + "webhook_" + selectedTrigger.id, }; workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "execution_argument", - value: '{"name": "value"}', + name: "tmp", + value: "webhook_" + selectedTrigger.id, + }; + workflow.triggers[selectedTriggerIndex].parameters[2] = { + name: "auth_headers", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "custom_response_body", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "await_response", + value: "v1", }; setWorkflow(workflow); - } else if (selectedTrigger.trigger_type === "WEBHOOK") { - if (workflow.triggers[selectedTriggerIndex] === undefined) { - return null; - } - - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow.triggers[selectedTriggerIndex].parameters.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "url", - value: referenceUrl + "webhook_" + selectedTrigger.id, - }; - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "tmp", - value: "webhook_" + selectedTrigger.id, - }; - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "auth_headers", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "custom_response_body", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "await_response", - value: "v1", - }; - setWorkflow(workflow); - } else { - // Always update - const newUrl = referenceUrl + "webhook_" + selectedTrigger.id; - //console.log("Validating webhook url: ", newUrl); - if (selectedTrigger.environment !== "cloud") { - if (newUrl !== workflow.triggers[selectedTriggerIndex].parameters[0].value) { - console.log("Url is wrong. NOT updating because of hybrid."); - //workflow.triggers[selectedTriggerIndex].parameters[0].value = newUrl; - //setWorkflow(workflow); - } - } - } - - trigger_header_auth = - workflow.triggers[selectedTriggerIndex].parameters.length > 2 - ? workflow.triggers[selectedTriggerIndex].parameters[2].value - : ""; - }else if( - selectedTrigger.trigger_type === "USERINPUT" - ){ - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow?.triggers[selectedTriggerIndex]?.parameters?.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "alertinfo", - value: "Do you want to continue the workflow? Start parameters: $exec", - }; - - // boolean, - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "options", - value: "boolean", - }; - - // email,sms,app ... - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "type", - value: "subflow", - }; - - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "email", - value: "test@test.com", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "sms", - value: "0000000", - }; - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "subflow", - value: "", - }; - - setWorkflow(workflow); + } else { + // Always update + const newUrl = referenceUrl + "webhook_" + selectedTrigger.id; + //console.log("Validating webhook url: ", newUrl); + if (selectedTrigger.environment !== "cloud") { + if (newUrl !== workflow.triggers[selectedTriggerIndex].parameters[0].value) { + console.log("Url is wrong. NOT updating because of hybrid."); + //workflow.triggers[selectedTriggerIndex].parameters[0].value = newUrl; + //setWorkflow(workflow); + } } } + + trigger_header_auth = + workflow.triggers[selectedTriggerIndex].parameters.length > 2 + ? workflow.triggers[selectedTriggerIndex].parameters[2].value + : ""; + } else if ( + selectedTrigger.trigger_type === "USERINPUT" + ) { + if ( + workflow.triggers[selectedTriggerIndex].parameters === undefined || + workflow.triggers[selectedTriggerIndex].parameters === null || + workflow?.triggers[selectedTriggerIndex]?.parameters?.length === 0 + ) { + workflow.triggers[selectedTriggerIndex].parameters = []; + workflow.triggers[selectedTriggerIndex].parameters[0] = { + name: "alertinfo", + value: "Do you want to continue the workflow? Start parameters: $exec", + }; + + // boolean, + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "options", + value: "boolean", + }; + + // email,sms,app ... + workflow.triggers[selectedTriggerIndex].parameters[2] = { + name: "type", + value: "subflow", + }; + + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "email", + value: "test@test.com", + }; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "sms", + value: "0000000", + }; + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "subflow", + value: "", + }; + + setWorkflow(workflow); + } + } } - const WebhookSidebar =!selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || workflow?.triggers[selectedTriggerIndex] === undefined || selectedTrigger?.trigger_type !== "WEBHOOK" ? null : -
    -

    - {selectedTrigger.app_name}: {selectedTrigger.status} -

    - - What are webhooks? - - +

    + {selectedTrigger.app_name}: {selectedTrigger.status} +

    + + What are webhooks? + + +
    Name
    + 0} + /> + {apps !== undefined && apps !== null && apps.length > 0 ? +
    + { + const lowercaseValue = inputValue.toLowerCase() + options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) + + return options + }} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return null; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + + return newname; + }} + options={sortByKey(apps, "name")} + fullWidth + onChange={(event, newValue) => { + // Workaround with event lol + if (newValue !== undefined && newValue !== null) { + var parsedvalue = JSON.parse(JSON.stringify(newValue)) + parsedvalue.actions = [] + parsedvalue.authentication = {} + selectedTrigger.app_association = parsedvalue + setUpdate(Math.random()); + } + }} + renderOption={(props, app, state) => { + var appname = app.name.replaceAll("_", " ") + appname = appname.charAt(0).toUpperCase() + appname.substring(1) + + return ( + + { + const newValue = app + + if (newValue !== undefined && newValue !== null) { + var parsedvalue = JSON.parse(JSON.stringify(newValue)) + parsedvalue.actions = [] + parsedvalue.authentication = {} + selectedTrigger.app_association = parsedvalue + selectedTrigger.large_image = app.large_image + + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedTrigger.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + setUpdate(Math.random()); + } + document.activeElement.blur(); + }} + > +
    + + {appname} + + + {appname} + +
    +
    +
    + ) + }} + renderInput={(params) => { + return ( + + ); }} /> -
    Name
    - + : null} + + {selectedTrigger.status === "running" || triggerEnvironments?.length < 2 ? null : +
    + Environment + +
    + } + +
    +
    + Parameters +
    +
    +
    + Webhook URI +
    +
    + { + }} + helperText={ + workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined && + workflow.triggers[selectedTriggerIndex].parameters[0].value !== null && + (workflow.triggers[ + selectedTriggerIndex + ].parameters[0].value.includes("localhost") || + workflow.triggers[ + selectedTriggerIndex + ].parameters[0].value.includes("127.0.0.1")) ? ( + + + PS: This does NOT work with localhost. Use your local IP + instead. + + ) : null + } + InputProps={{ + style: { + }, + endAdornment: + + { + var copyText = document.getElementById("webhook_uri_field"); + if (copyText !== undefined && copyText !== null) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(copyText.value); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied Webhook URL"); + } else { + console.log("Couldn't find webhook URI field: ", copyText); + } + }} + edge="end" + > + + + + }} + fullWidth + disabled + value={ + workflow.triggers[selectedTriggerIndex].parameters[0].value + } color="primary" - placeholder={selectedTrigger.label} - onChange={selectedTriggerChange} + placeholder="10" + onBlur={(e) => { + setTriggerCronWrapper(e.target.value); + }} /> - {apps !== undefined && apps !== null && apps.length > 0 ? -
    - { - const lowercaseValue = inputValue.toLowerCase() - options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) - - return options - }} - getOptionLabel={(option) => { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return null; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - - return newname; - }} - options={sortByKey(apps, "name")} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette?.borderRadius, - }} - onChange={(event, newValue) => { - // Workaround with event lol - console.log("CHANGE: ", event, newValue) - if (newValue !== undefined && newValue !== null) { - var parsedvalue = JSON.parse(JSON.stringify(newValue)) - parsedvalue.actions = [] - parsedvalue.authentication = {} - selectedTrigger.app_association = parsedvalue - setUpdate(Math.random()); - } - }} - renderOption={(props, app, state) => { - var appname = app.name.replaceAll("_", " ") - appname = appname.charAt(0).toUpperCase() + appname.substring(1) - - return ( - - { - console.log("CLICK: ", app) - const newValue = app - - if (newValue !== undefined && newValue !== null) { - var parsedvalue = JSON.parse(JSON.stringify(newValue)) - parsedvalue.actions = [] - parsedvalue.authentication = {} - selectedTrigger.app_association = parsedvalue - selectedTrigger.large_image = app.large_image - - if (cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(selectedTrigger.id) - if (foundnode !== undefined && foundnode !== null) { - foundnode.data("large_image", app.large_image) - } - } - - setUpdate(Math.random()); - } - document.activeElement.blur(); - }} - > -
    - - {appname} - - - {appname} - -
    -
    -
    - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - -
    - : null} - {selectedTrigger.status === "running" ? null : -
    - Environment - -
    - } +
    + + +
    -
    -
    - Parameters -
    -
    -
    - Webhook URI -
    -
    - { - }} - helperText={ - workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined && - workflow.triggers[selectedTriggerIndex].parameters[0].value !== null && - (workflow.triggers[ - selectedTriggerIndex - ].parameters[0].value.includes("localhost") || - workflow.triggers[ - selectedTriggerIndex - ].parameters[0].value.includes("127.0.0.1")) ? ( - - PS: This does NOT work with localhost. Use your local IP - instead. - - ) : null - } - InputProps={{ - style: { - }, - endAdornment: - - { - var copyText = document.getElementById("webhook_uri_field"); - if (copyText !== undefined && copyText !== null) { - console.log("NAVIGATOR: ", navigator); - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } - - navigator.clipboard.writeText(copyText.value); - copyText.select(); - copyText.setSelectionRange( - 0, - 99999 - ); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - toast("Copied Webhook URL"); - } else { - console.log("Couldn't find webhook URI field: ", copyText); - } - }} - edge="end" - > - - - - }} - fullWidth - disabled - value={ - workflow.triggers[selectedTriggerIndex].parameters[0].value - } - color="primary" - placeholder="10" - onBlur={(e) => { - setTriggerCronWrapper(e.target.value); - }} - /> -
    - - -
    - -
    -
    -
    - Authentication headers -
    -
    -
    - { }} - InputProps={{ - style: { - }, - }} - fullWidth - multiline - rows="4" - defaultValue={trigger_header_auth} - color="primary" - disabled={selectedTrigger.status === "running"} - placeholder={"AUTH_HEADER=AUTH_VALUE1"} - onBlur={(e) => { - const value = e.target.value; - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - workflow.triggers[selectedTriggerIndex].parameters[2] = { - value: value, - name: "auth_headers", - }; - setWorkflow(workflow); - }} - /> -
    -
    -
    -
    - Custom Response -
    -
    -
    - { }} - InputProps={{ - style: { - }, - }} - fullWidth - multiline - rows="2" - color="primary" - disabled={selectedTrigger.status === "running"} - placeholder={"OK"} - onBlur={(e) => { - const value = e.target.value; - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - workflow.triggers[selectedTriggerIndex].parameters[3] = { - value: value, - name: "custom_response_body", - }; - setWorkflow(workflow); - }} - /> -
    - {workflow.triggers[selectedTriggerIndex].parameters.length > 4 ? - - { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - // Sets the webhook to run as version 2.. kinda - var value = "v2" - if (workflow.triggers[selectedTriggerIndex].parameters[4].value.includes("v2")) { - value = "v1" - } - - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "await_response", - value: value - } - - setWorkflow(workflow) - setUpdate(Math.random()) - }} - color="primary" - value="await_response" - /> - } - label={
    Wait For Response
    } - /> -
    - : null} -
    +
    +
    +
    + Authentication headers +
    +
    + { }} + InputProps={{ + style: { + }, + }} + fullWidth + multiline + rows="4" + defaultValue={trigger_header_auth} + color="primary" + disabled={selectedTrigger.status === "running"} + placeholder={"AUTH_HEADER=AUTH_VALUE1"} + onBlur={(e) => { + const value = e.target.value; + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + workflow.triggers[selectedTriggerIndex].parameters[2] = { + value: value, + name: "auth_headers", + }; + setWorkflow(workflow); + }} + /> +
    +
    +
    +
    + Custom Response +
    +
    +
    + { }} + InputProps={{ + style: { + }, + }} + fullWidth + multiline + rows="2" + color="primary" + disabled={selectedTrigger.status === "running"} + placeholder={"OK"} + onBlur={(e) => { + const value = e.target.value; + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + workflow.triggers[selectedTriggerIndex].parameters[3] = { + value: value, + name: "custom_response_body", + }; + setWorkflow(workflow); + }} + /> +
    + {workflow.triggers[selectedTriggerIndex].parameters.length > 4 ? + + { + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + // Sets the webhook to run as version 2.. kinda + var value = "v2" + if (workflow.triggers[selectedTriggerIndex].parameters[4].value.includes("v2")) { + value = "v1" + } + + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "await_response", + value: value + } + + setWorkflow(workflow) + setUpdate(Math.random()) + }} + color="primary" + value="await_response" + /> + } + label={
    Wait For Response
    } + /> +
    + : null}
    +
    +
    const stopMailSub = (trigger, triggerindex) => { // DELETE @@ -15101,7 +16452,7 @@ const releaseToConnectLabel = "Release to Connect" } - // Version: v2 = await response for 30 sec + // Version: v2 = await response for 30 sec const await_resp = trigger.parameters.find((param) => param.name === "await_response"); var version = ""; if (await_resp !== undefined && await_resp !== null) { @@ -15125,15 +16476,22 @@ const releaseToConnectLabel = "Release to Connect" environment: trigger.environment, auth: auth, custom_response: custom_response, - version: version, - version_timeout: 15, - }; + version: version, + version_timeout: 15, + } - console.log("Trigger data: ", data) + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } fetch(globalUrl + "/api/v1/hooks/new", { method: "POST", - headers: { "content-type": "application/json" }, + headers: headers, body: JSON.stringify(data), credentials: "include", }) @@ -15147,6 +16505,8 @@ const releaseToConnectLabel = "Release to Connect" workflow.triggers[selectedTriggerIndex].status = "running"; setWorkflow(workflow); saveWorkflow(workflow); + + loadTriggers(workflow.org_id) } else { toast("Failed starting webhook: " + responseJson.reason); } @@ -15161,39 +16521,57 @@ const releaseToConnectLabel = "Release to Connect" if (trigger.id === undefined) { return; } - - fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", { + + // Unselect everything in cytoscape + if (cy !== undefined && cy !== null) { + cy.$(":selected").unselect() + } + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + console.log("ORGID: ", workflow.org_id) + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + const url = `${globalUrl}/api/v1/hooks/${trigger.id}/delete` + fetch(url, { method: "DELETE", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } - + return response.json(); }) .then((responseJson) => { if (!responseJson.success) { if (responseJson.reason !== undefined) { - toast("Failed to stop webhook: " + responseJson.reason); - } + toast.error("Failed to stop webhook: " + responseJson.reason); + } else { + //toast.error("Failed to stop webhook. Please try again, or contact support@shuffler.io to get it sorted."); + } } else { toast("Successfully stopped webhook"); + + loadTriggers(workflow.org_id) } if (workflow.triggers[triggerindex] !== undefined) { workflow.triggers[triggerindex].status = "stopped"; } - trigger.status = "stopped"; - setSelectedTrigger(trigger); - setWorkflow(workflow); - saveWorkflow(workflow); + + trigger.status = "stopped" + setSelectedTrigger(trigger) + setWorkflow(workflow) + saveWorkflow(workflow) setSelectedTrigger({}) - + }) .catch((error) => { //toast(error.toString()); @@ -15202,76 +16580,84 @@ const releaseToConnectLabel = "Release to Connect" ); }); }; - + // POST to /api/v1/workflows const createWorkflow = (workflow, trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(workflow), - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - getAvailableWorkflows(trigger_index) - } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0) { - toast("Successfully created workflow"); + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } - handleWorkflowSelectionUpdate({ target: { value: responseJson } }, true) - } - }) - .catch((error) => { - console.log("Create workflow error: ", error.toString()) - }) + fetch(globalUrl + "/api/v1/workflows", { + method: "POST", + headers: headers, + body: JSON.stringify(workflow), + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getAvailableWorkflows(trigger_index) + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0) { + toast("Successfully created workflow"); + + handleWorkflowSelectionUpdate({ target: { value: responseJson } }, true) + } + }) + .catch((error) => { + console.log("Create workflow error: ", error.toString()) + }) } - const UserinputSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] || !selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "USERINPUT" ? null : -
    -

    - {selectedTrigger.app_name} -

    - - What is the user input trigger? - - -
    Name
    - + const UserinputSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] || !selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "USERINPUT" ? null : +
    +

    + {selectedTrigger.app_name} +

    + + What is the user input trigger? + + +
    Name
    + 0} + /> - {/*
    + {/*
    Environment:
    */} -
    -
    -
    - Information - - The information you want to show the user. Supports variables. Supports Markdown & HTML. - -
    +
    +
    +
    + Information + + The information you want to show the user. Supports variables. Supports Markdown & HTML. + +
    +
    + 0 && workflow.triggers[selectedTriggerIndex].parameters[0] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[0].value : "" + } + color="primary" + placeholder="" + onBlur={(e) => { + setTriggerTextInformationWrapper(e.target.value); + }} + /> +
    +
    + Input options + + Use subflows to connect to any app you want, or use the default email and sms options + +
    +
    + + { + setTriggerOptionsWrapper("subflow"); + }} + color="primary" + value="subflow" + /> + } + label={
    Subflow
    } + /> + { + setTriggerOptionsWrapper("email"); + }} + color="primary" + value="email" + /> + } + label={
    Email
    } + /> + 0 && workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") : false} + onChange={() => { + setTriggerOptionsWrapper("sms"); + }} + color="primary" + value="sms" + disabled={true} + /> + } + label={
    SMS
    } + /> +
    + {workflow?.triggers && + workflow?.triggers[selectedTriggerIndex] && + workflow?.triggers[selectedTriggerIndex].parameters && + workflow?.triggers[selectedTriggerIndex].parameters[2] && + workflow?.triggers[selectedTriggerIndex].parameters[2].value && + workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow") + ? ( +
    + {workflows === undefined || + workflows === null || + workflows.length === 0 ? null : ( + option.id === value.id} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.name === undefined || option.name === null) { + return "No Workflow Selected"; + } + + const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " "); + return newname; + }} + options={ + [{ + "id": "", + "name": "No Workflow Selected", + }].concat(workflows) + } + fullWidth + onChange={(event, newValue) => { + console.log("Changed autocomplete!") + handleWorkflowSelectionUpdate({ target: { value: newValue } }, true) + event.target.blur(); + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose Trigger '{data.name}' + + + }> + { + handleWorkflowSelectionUpdate({ + target: { + value: data, + } + }, + true) + document.activeElement.blur(); + }} + > + + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + )} + + {/* Button for making a new workflow to attach */} + +
    + ) : null} + + {workflow?.triggers && + workflow?.triggers[selectedTriggerIndex] && + workflow?.triggers[selectedTriggerIndex].parameters && + workflow?.triggers[selectedTriggerIndex].parameters[2] && + workflow?.triggers[selectedTriggerIndex].parameters[2].value && + workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("email") + ? ( 0 && workflow.triggers[selectedTriggerIndex].parameters[0] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[0].value : "" - } + label="Email" color="primary" - placeholder="" - onBlur={(e) => { - setTriggerTextInformationWrapper(e.target.value); + required + placeholder={"mail1@company.com,mail2@company.com"} + defaultValue={ + workflow.triggers[selectedTriggerIndex].parameters[3].value + } + onBlur={(event) => { + workflow.triggers[selectedTriggerIndex].parameters[3].value = + event.target.value; + setWorkflow(workflow); + setUpdate(Math.random()); }} /> -
    -
    - Input options - - Use subflows to connect to any app you want, or use the default email and sms options - -
    -
    - - { + workflow.triggers[selectedTriggerIndex].parameters[4].value = + event.target.value; + setWorkflow(workflow); + setUpdate(Math.random()); + }} + /> + ) : null} + + +
    + Required Input-Questions + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
    + {workflow.input_questions.map((question, index) => { + var foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "input_questions") + + const selectionClick = () => { + if (foundParamIndex === -1) { + workflow.triggers[selectedTriggerIndex].parameters.push({ + "name": "input_questions", + "value": [], + }) + + foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1 + } else { + try { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.parse(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) + } catch (e) { + console.log("Couldn't parse input questions: ", e) + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = [] + } + } + + if (workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.includes(question.name)) { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.filter((item) => item !== question.name) + } else { + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.push(question.name) + } + + // Make it back to a string + workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.stringify(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) + setWorkflow(workflow) + setUpdate(Math.random()) + } + + return ( +
    { + selectionClick() + }}> { - setTriggerOptionsWrapper("subflow"); - }} - color="primary" - value="subflow" + checked={foundParamIndex !== -1 ? workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.includes(question.name) : false} /> - } - label={
    Subflow
    } - /> - { - setTriggerOptionsWrapper("email"); - }} - color="primary" - value="email" - /> - } - label={
    Email
    } - /> - 0 && workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") : false} - onChange={() => { - setTriggerOptionsWrapper("sms"); - }} - color="primary" - value="sms" - disabled={true} - /> - } - label={
    SMS
    } - /> - - {workflow?.triggers && - workflow?.triggers[selectedTriggerIndex] && - workflow?.triggers[selectedTriggerIndex].parameters && - workflow?.triggers[selectedTriggerIndex].parameters[2] && - workflow?.triggers[selectedTriggerIndex].parameters[2].value && - workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow") - ? ( -
    - {workflows === undefined || - workflows === null || - workflows.length === 0 ? null : ( - option.id === value.id} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.name === undefined || option.name === null) { - return "No Workflow Selected"; - } - - const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " "); - return newname; - }} - options={ - [{ - "id": "", - "name": "No Workflow Selected", - }].concat(workflows) - } - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette?.borderRadius, - marginTop: 15, - marginBottom: 15, - }} - onChange={(event, newValue) => { - console.log("Changed autocomplete!") - handleWorkflowSelectionUpdate({ target: { value: newValue } }, true) - event.target.blur(); - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } - - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose Trigger '{data.name}' - - - }> - { - handleWorkflowSelectionUpdate({ - target: { - value: data, - }}, - true) - document.activeElement.blur(); - }} - > - - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - )} - - {/* Button for making a new workflow to attach */} - - -
    - ) : null} - - {workflow?.triggers && - workflow?.triggers[selectedTriggerIndex] && - workflow?.triggers[selectedTriggerIndex].parameters && - workflow?.triggers[selectedTriggerIndex].parameters[2] && - workflow?.triggers[selectedTriggerIndex].parameters[2].value && - workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("email") - ? ( - { - workflow.triggers[selectedTriggerIndex].parameters[3].value = - event.target.value; - setWorkflow(workflow); - setUpdate(Math.random()); - }} - /> - ) : null} - - {workflow?.triggers && - workflow?.triggers[selectedTriggerIndex] && - workflow?.triggers[selectedTriggerIndex].parameters && - workflow?.triggers[selectedTriggerIndex].parameters[2] && - workflow?.triggers[selectedTriggerIndex].parameters[2].value && - ( - workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("email") || - workflow?.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") - ) ? ( - { - workflow.triggers[selectedTriggerIndex].parameters[4].value = - event.target.value; - setWorkflow(workflow); - setUpdate(Math.random()); - }} - /> - ) : null} - - -
    - Required Input-Questions - {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? -
    - {workflow.input_questions.map((question, index) => { - var foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "input_questions") - - const selectionClick = () => { - if (foundParamIndex === -1) { - workflow.triggers[selectedTriggerIndex].parameters.push({ - "name": "input_questions", - "value": [], - }) - - foundParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1 - } else { - try { - workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.parse(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) - } catch (e) { - console.log("Couldn't parse input questions: ", e) - workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = [] - } - } - - if (workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.includes(question.name)) { - workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.filter((item) => item !== question.name) - } else { - workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value.push(question.name) - } - - // Make it back to a string - workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value = JSON.stringify(workflow.triggers[selectedTriggerIndex].parameters[foundParamIndex].value) - setWorkflow(workflow) - setUpdate(Math.random()) - } - - return ( -
    { - selectionClick() - }}> - - - {question.name} - -
    - ) - })} -
    - : -
    { - setEditWorkflowModalOpen(true) - toast.info("Expand and scroll down to add input-questions") - }}> - No Input-Questions found. Click to add them! -
    - } + + {question.name} + +
    + ) + })}
    + : +
    { + setEditWorkflowModalOpen(true) + toast.info("Expand and scroll down to add input-questions") + }}> + No Input-Questions found. Click to add them! +
    + } +
    -
    +
    const defaultEnvironment = environments.find( (env) => env.default && env.Name.toLowerCase() !== "cloud" ); if (selectedTrigger.trigger_type === "PIPELINE" && selectedTrigger.environment === "onprem" && defaultEnvironment !== undefined) { - selectedTrigger.environment = defaultEnvironment.Name - setSelectedTrigger(selectedTrigger) } + selectedTrigger.environment = defaultEnvironment.Name + setSelectedTrigger(selectedTrigger) + } - const PipelineSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] || !selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "SCHEDULE" ? null : -
    -

    - {selectedTrigger.app_name}: {selectedTrigger.status} -

    - - What are pipelines? - - -
    Name
    - + const PipelineSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] || !selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "SCHEDULE" ? null : +
    +

    + {selectedTrigger.app_name}: {selectedTrigger.status} +

    + + What are pipelines? + + +
    Name
    + -
    - Environment - { + selectedTrigger.environment = e.target.value; + setSelectedTrigger(selectedTrigger); - setWorkflow(workflow); - setUpdate(Math.random()); - }} + setWorkflow(workflow); + setUpdate(Math.random()); + }} + style={{ + backgroundColor: theme.palette.inputColor, + color: "white", + height: 50, + }} + > + {environments.map((data) => { + if (data.archived) { + return null; + } + + if (data.Name.toLowerCase() === "cloud") { + return null; + } + + return ( + - {environments.map((data) => { - if (data.archived) { - return null; - } - - if (data.Name.toLowerCase() === "cloud") { - return null; - } - - return ( - - {data.Name} - - ); - })} - -
    - -
    -
    - What would you like to do? - {/* + {data.Name} + + ); + })} + +
    + +
    +
    + What would you like to do? + {/*
    { @@ -15866,94 +17250,193 @@ const releaseToConnectLabel = "Release to Connect" */} -
    { - if (selectedTrigger.status === "running"){ - toast("please stop the trigger to edit the configuration"); - return; - } else { - setSelectedOption("Kafka Queue"); - setTenzirConfigModalOpen(true); - } - }} - style={{ - border: "1px solid rgba(255,255,255,0.3)", - borderRadius: theme.palette?.borderRadius, - padding: 10, - cursor: "pointer", - marginTop: 5, - display: "flex", - alignItems: "center", - }} - > - { - if (selectedTrigger.status !== "running") { - setSelectedOption("Kafka Queue") +
    { + if (selectedTrigger.status === "running") { + toast("please stop the trigger to edit the configuration"); + return; + } else { + setSelectedOption("Kafka Queue"); + setTenzirConfigModalOpen(true); + } + }} + style={{ + border: "1px solid rgba(255,255,255,0.3)", + borderRadius: theme.palette?.borderRadius, + padding: 10, + cursor: "pointer", + marginTop: 5, + display: "flex", + alignItems: "center", + }} + > + { + if (selectedTrigger.status !== "running") { + setSelectedOption("Kafka Queue") - } - }} - - value={"Kafka Queue"} - name="option" - /> } - label="Subscribe to a Kafka Queue" - /> -
    + }} -
    - -
    -
    -
    + value={"Kafka Queue"} + name="option" + /> + } + label="Subscribe to a Kafka Queue" + />
    - const ScheduleSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] && (!selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "SCHEDULE") ? null : -
    -

    - {selectedTrigger.app_name}: {selectedTrigger.status} -

    - - What are schedules? - - + +
    +
    +
    +
    + + const ScheduleSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] && (!selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "SCHEDULE") ? null : +
    +

    + {selectedTrigger.app_name}: {selectedTrigger.status} +

    + + What are schedules? + + +
    Name
    + 0} + /> +
    + Environment + +
    + +
    +
    + Parameters +
    -
    Name
    + > +
    +
    + When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"} +
    +
    { + setTriggerCronWrapper(e.target.value); + }} /> -
    - Environment - + /> +
    + Runtime Argument: +
    + 1 ? workflow.triggers[selectedTriggerIndex].parameters[1].value : "" + } + placeholder='{"key": "value"}' + onBlur={(e) => { + setTriggerBodyWrapper(e.target.value); + }} + /> -
    -
    - Parameters -
    -
    -
    - When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"} -
    -
    - { - setTriggerCronWrapper(e.target.value); - }} - /> - {/*selectedTrigger.environment === "cloud" ? - - : - null - */} -
    -
    -
    - Runtime Argument: -
    -
    - 1 ? workflow.triggers[selectedTriggerIndex].parameters[1].value : "" - } - placeholder='{"key": "value"}' - onBlur={(e) => { - setTriggerBodyWrapper(e.target.value); - }} - /> - -
    - - -
    -
    +
    + +
    +
    +
    const cytoscapeViewWidths = isMobile ? 50 : 950; const bottomBarStyle = { @@ -16177,24 +17564,24 @@ const releaseToConnectLabel = "Release to Connect" marginLeft: 20, marginBottom: 30, zIndex: 10, - transform: isMobile - ? `translateX(20px)` - : `translateX(${leftBarSize}px)`, - top: isMobile ? appBarSize + 55 : undefined, + transform: isMobile + ? `translateX(20px)` + : `translateX(${leftBarSize}px)`, + top: isMobile ? appBarSize + 55 : undefined, bottom: isMobile ? undefined : 0, -}; + }; const topBarStyle = { position: "absolute", top: isMobile ? 30 : 25, - transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, + transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, transition: "all 0.3s ease", - zoom: 0.9, + zoom: 0.9, } const TopCytoscapeBar = (props) => { - const [hovered, setHovered] = useState(false) + const [hovered, setHovered] = useState(false) if (workflow.public === true) { return null @@ -16204,37 +17591,41 @@ const releaseToConnectLabel = "Release to Connect" return null } - const isCorrectOrg = workflow.public === true || userdata.active_org.id === undefined || userdata.active_org.id === null || workflow.org_id === null || workflow.org_id === undefined || workflow.org_id.length === 0 || userdata.active_org.id === workflow.org_id + const isCorrectOrg = workflow.public === true || userdata.active_org.id === undefined || userdata.active_org.id === null || workflow.org_id === null || workflow.org_id === undefined || workflow.org_id.length === 0 || userdata.active_org.id === workflow.org_id return (
    -
    - { - setHovered(true) - }} - onMouseLeave={() => { - setHovered(false) - }} - onClick={() => { - setEditWorkflowModalOpen(true) - setLastSaved(false) - }} - > - - {workflow.name} - +
    + { + setHovered(true) + }} + onMouseLeave={() => { + setHovered(false) + }} + onClick={() => { + setEditWorkflowModalOpen(true) + setLastSaved(false) + }} + > + {workflow.name !== undefined && workflow.name !== null && workflow.name.length > 0 ? + + : + null + } + {workflow.name} + {workflowAsCode && ( + : userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 ? + + : null + : - - Warning: This workflow is controlled by your parent org and may not be editable. - - } - {originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0 || originalWorkflow.suborg_distribution.includes("none") ? null : - + + Save the workflow first + + : null} arrow placement="right"> - - Select an Org - - { + if (lastSaved === false && originalWorkflow.id === workflow.id) { + setSuborgWorkflows([]) - // Unselect in cy - if (cy !== undefined && cy !== null) { - cy.nodes().unselect() - cy.edges().unselect() - } + saveWorkflow(workflow, undefined, undefined, e.target.value) - ReactDOM.unstable_batchedUpdates(() => { - getEnvironments(e.target.value) - getAppAuthentication(undefined, undefined, undefined, e.target.value) - getFiles(e.target.value) - listOrgCache(e.target.value) + /* Standard re-loads */ + setAllTriggers(undefined) + setSelectedTriggerIndex(-1) - // FIXME: There is a timing problem here. - // For events to have the data they need, they - // need to be registered with setupGraph() - // AFTER all the APIs are done + getEnvironments(e.target.value) + getAppAuthentication(undefined, undefined, undefined, e.target.value) + getFiles(e.target.value) + listOrgCache(e.target.value) + /* Standard re-loads */ - // Should look through childorg workflow - setTimeout(() => { - if (e.target.value === originalWorkflow.org_id) { - console.log("Original org selected. No change.") + toast.warn(`Saving workflow first due to detected changes.`, { + autoClose: 2000, + }) - updateCurrentWorkflow(originalWorkflow) - return - } else { - // Load environments, auth, auth groups - //toast("Loading correct info for suborg") - } - if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { - //console.log("Childorg doesn't exist (?). Suborgworkflows: ", suborgWorkflows) + return + } - if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { - var found = false - for (var suborgkey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgkey] - if (suborgWorkflow.org_id === e.target.value) { - found = true - updateCurrentWorkflow(suborgWorkflow) - break - } - } + if (workflow.org_id === e.target.value) { + console.log("Same org selected. No change.") + return + } else { + //if (savingState === 0) { + // saveWorkflow(workflow, undefined, undefined, undefined) + // return + //} + } - if (!found) { - toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") - //console.log("No workflow found out of suborg workflows.") - - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("Suborgworkflows: ", suborgWorkflows) - toast("(1) Loading NEW workflow for this org (?). Please wait a second.") - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("In childorg EXIST!") - var workflowFound = false - for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { - const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] - for (var suborgWorkflowKey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] - if (suborgWorkflow.org_id === e.target.value) { - workflowFound = true + navigate(`?org_id=${e.target.value}`) - updateCurrentWorkflow(suborgWorkflow) - break - } - } + // Unselect in cy + if (cy !== undefined && cy !== null) { + cy.nodes().unselect() + cy.edges().unselect() + } - if (workflowFound) { - break - } - } + ReactDOM.unstable_batchedUpdates(() => { + /* Standard re-loads */ + setAllTriggers(undefined) + setSelectedTriggerIndex(-1) + getEnvironments(e.target.value) + getAppAuthentication(undefined, undefined, undefined, e.target.value) + getFiles(e.target.value) + listOrgCache(e.target.value) + /* Standard re-loads */ - if (!workflowFound) { - console.log("No workflow found.") - toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } - }, 500) - }) - }} - label="Suborg Distribution" - fullWidth - > - - Parent: {userdata.active_org.large_image}{" "} - - {userdata.active_org.name} - - + // Reset the save button to ensure random saves don't occur during move + setLastSaved(true) - + // FIXME: There is a timing problem here. + // For events to have the data they need, they + // need to be registered with setupGraph() + // AFTER all the APIs are done - {originalWorkflow.suborg_distribution.map((org_id, index) => { - var data = {} - for (var key in userdata.orgs) { - if (userdata.orgs[key].id === org_id) { - data = userdata.orgs[key] - break + // Should look through childorg workflow + setTimeout(() => { + if (e.target.value === originalWorkflow.org_id) { + updateCurrentWorkflow(originalWorkflow) + return + } else { + // Load environments, auth, auth groups + //toast("Loading correct info for suborg") + } + + if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { + //console.log("Childorg doesn't exist (?). Suborgworkflows: ", suborgWorkflows) + + if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { + var found = false + for (var suborgkey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgkey] + if (suborgWorkflow.org_id === e.target.value) { + found = true + updateCurrentWorkflow(suborgWorkflow) + break + } + } + + if (!found) { + toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") + //console.log("No workflow found out of suborg workflows.") + + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } else { + console.log("Suborgworkflows: ", suborgWorkflows) + toast("(1) Loading NEW workflow for this org (?). Please wait a second.") + saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } else { + console.log("In childorg EXIST!") + + var workflowFound = false + for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { + const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] + for (var suborgWorkflowKey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] + if (suborgWorkflow.org_id === e.target.value) { + workflowFound = true + + updateCurrentWorkflow(suborgWorkflow) + break + } + } + + if (workflowFound) { + break + } + } + + if (!workflowFound) { + console.log("No workflow found.") + toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } + }, 500) + }) + }} + label="Suborg Distribution" + fullWidth + > + + + { + e.preventDefault() + e.stopPropagation() + }} + /> {userdata.active_org.large_image}{" "} + + + {userdata.active_org.name} + + + + + + {originalWorkflow.suborg_distribution.map((org_id, index) => { + var data = {} + for (var key in userdata.orgs) { + if (userdata.orgs[key].id === org_id) { + data = userdata.orgs[key] + break + } + } + + if (data.id === undefined || data.id === null) { + return null + } + + var skipOrg = false; + + const imagesize = 22 + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: + data.creator_org !== undefined && + data.creator_org !== null && + data.creator_org.length > 0 + ? data.id === userdata.active_org.id + ? 0 + : 0 + : 0, + } + + const image = + data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ) + + var orgDiff = { + different: false, } - } - if (data.id === undefined || data.id === null) { - //toast("No org found for id: " + org_id) - return null - } + const foundMatchingWorkflow = suborgWorkflows?.find((workflow) => workflow.org_id === data.id) + if (foundMatchingWorkflow !== undefined && foundMatchingWorkflow !== null && foundMatchingWorkflow.diff !== undefined && foundMatchingWorkflow.diff !== null) { + orgDiff = foundMatchingWorkflow.diff + } - var skipOrg = false; + //console.log("DIFF: ", orgDiff) - const imagesize = 22 - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - marginRight: 10, - marginLeft: - data.creator_org !== undefined && - data.creator_org !== null && - data.creator_org.length > 0 - ? data.id === userdata.active_org.id - ? 0 - : 0 - : 0, - } - - const image = - data.image === "" ? ( - {data.name} - ) : ( - {data.name} - ) - - - return ( - + return ( + + {image}{" "} {data.name} - - ) - })} - - - } -
    + - {showEnvironment === true && environments.length > 1 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? - + {foundMatchingWorkflow !== undefined && foundMatchingWorkflow !== null && foundMatchingWorkflow?.errors !== undefined && foundMatchingWorkflow?.errors !== null && foundMatchingWorkflow?.errors?.length > 0 && + + + {foundMatchingWorkflow.errors.length} Workflow Issue{foundMatchingWorkflow.errors.length > 1 ? "s" : ""} + +
    + } + > + + + } - - Location - - + + + } +
    + + {showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? + 0 && Object.getOwnPropertyNames(selectedActionEnvironment).length !== 0 && selectedActionEnvironment?.Name !== "Cloud" && savingState === 0 ? ( + +
    + 0} + onChange={(event) => { + toast("Opening in new tab. Refresh this page after adding it.") + + setTimeout(() => { + window.open(`/admin?tab=locations`, "_blank", "noopener,noreferrer") + }, 1500) + //selectedActionEnvironment.suborg_distribution = event.target.checked ? userdata.orgs.map((org) => org.id) : [] + //changeDistribution(selectedAction?.selectedAuthentication) }} - - - /> - - - : null} - - {data.default === true ? - - : null} - - - {data.Name} - - ); - })} - - - : null} - - - {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : - + + ) : null + } placement="right"> - } placement="bottom"> - { - console.log("Click: ", wf) - }}> - - - - - ) - })} -
    - } + + + + Runtime Location + + + +
    + : null} + + + {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : + + }
    ); @@ -16814,7 +18440,7 @@ const releaseToConnectLabel = "Release to Connect" }; const handleActionHover = (inside, actionId) => { - if (cy !== undefined) { + if (cy !== undefined && cy !== null) { var node = cy.getElementById(actionId); if (node.length > 0) { if (inside) { @@ -16890,163 +18516,164 @@ const releaseToConnectLabel = "Release to Connect" }; const BottomAvatars = () => { - const connectedUsers = [{ - "user": "Anonymous", - "user_id": "user_id", - "color": "blue", - }] - + const connectedUsers = [{ + "user": "Anonymous", + "user_id": "user_id", + "color": "blue", + }] - if (connectedUsers === undefined || connectedUsers === null || connectedUsers.length < 2) { - return null - } - const avatarStyle = { - position: "fixed", - display: "flex", - right: isMobile ? 20 : 20, - top: isMobile ? appBarSize-100 : undefined, - bottom: isMobile ? undefined : 0, - left: isMobile ? undefined : leftBarSize, - minWidth: cytoscapeViewWidths, - maxWidth: cytoscapeViewWidths, - marginLeft: 20, - marginBottom: 20, - zIndex: 50, - } + if (connectedUsers === undefined || connectedUsers === null || connectedUsers.length < 2) { + return null + } - const HandleAvatar = (props) => { - const {user} = props - console.log("Clicked avatar: ", user) + const avatarStyle = { + position: "fixed", + display: "flex", + right: isMobile ? 20 : 20, + top: isMobile ? appBarSize - 100 : undefined, + bottom: isMobile ? undefined : 0, + left: isMobile ? undefined : leftBarSize, + minWidth: cytoscapeViewWidths, + maxWidth: cytoscapeViewWidths, + marginLeft: 20, + marginBottom: 20, + zIndex: 50, + } - const userTitle = user.user[0].toUpperCase() - return ( - - - {userTitle} - - - ) - } + const HandleAvatar = (props) => { + const { user } = props + console.log("Clicked avatar: ", user) + const userTitle = user.user[0].toUpperCase() return ( -
    - {connectedUsers.map((user) => { - return ( - - ) - })} -
    - ) + + + {userTitle} + + + ) + } + + return ( +
    + {connectedUsers.map((user) => { + return ( + + ) + })} +
    + ) } - const shownErrors = !distributedFromParent && !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ? -
    0 && showErrors && (!workflow.public || userdata.support === true) ? +
    + color: "white", + padding: 10, + borderRadius: theme.palette?.borderRadius, + transition: "left 0.3s ease, top 0.3s ease", + }} + > - + { + e.preventDefault(); + + // A temporary hider thing + setShowErrors(false) + }} > - { - e.preventDefault(); + + + - // A temporary hider thing - setShowErrors(false) - }} - > - - - + + {/**/} + {workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""} + + + {workflow.errors.slice(0, 3).map((error, index) => { + // Loop through each word, and if it matches "Action " then replace it with a link to the action + var colornext = false + const newerror = error === undefined || error == null ? "" : error.split(" ").map((word) => { + if (colornext) { + colornext = false + return ( + { + // Find it in cytoscape + if (cy === undefined || cy === null) { + return + } - - {/**/} - {workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""} - - - {workflow.errors.slice(0,3).map((error) => { - // Loop through each word, and if it matches "Action " then replace it with a link to the action - var colornext = false - const newerror = error === undefined || error == null ? "" : error.split(" ").map((word) => { - if (colornext) { - colornext = false - return ( - { - // Find it in cytoscape - if (cy === undefined || cy === null) { - return - } + const foundnode = cy.nodes().filter((node) => { + const nodelabel = node.data("label") + if (nodelabel === undefined || nodelabel === null) { + return false + } - const foundnode = cy.nodes().filter((node) => { - const nodelabel = node.data("label") - if (nodelabel === undefined || nodelabel === null) { - return false - } + return nodelabel.toLowerCase() === word.toLowerCase() + }) - return nodelabel.toLowerCase() === word.toLowerCase() - }) + if (foundnode === undefined || foundnode === null || foundnode.length === 0) { + return + } - if (foundnode === undefined || foundnode === null || foundnode.length === 0) { - return - } + cy.elements().unselect() + foundnode[0].select() + }} + > + {word}  + + ) + } - cy.elements().unselect() - foundnode[0].select() - }} - > - {word}  - - ) - } + if (word.toLowerCase() === "action") { + colornext = true + } - if (word.toLowerCase() === "action") { - colornext = true - } + return word + " " + }) - return word + " " - }) + if (newerror === undefined || newerror === null || newerror === "") { + return null + } - if (newerror === undefined || newerror === null || newerror === "") { - return null - } - - return ( -
    - - {newerror} -
    - ) - })} -
    -
    - : null + return ( +
    + - {newerror} +
    + ) + })} + +
    + : null const RightsideBar = () => { - const [hovered, setHovered] = useState(false) + const [hovered, setHovered] = useState(false) useEffect(() => { const handleKeyDown = (event) => { @@ -17074,33 +18701,33 @@ const releaseToConnectLabel = "Release to Connect" } } - if (( event.ctrlKey || event.metaKey ) && event.key === ";") { + if ((event.ctrlKey || event.metaKey) && event.key === ";") { if (!workflow.public && executionModalOpen) { getWorkflowExecution(props.match.params.key, ""); } } - /* - if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { - console.log("Shift key pressed") - if (!workflow.public && executionModalOpen) { - setExecutionRunning(false); - stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) - setExecutionModalView(0); - } - } - */ + /* + if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { + console.log("Shift key pressed") + if (!workflow.public && executionModalOpen) { + setExecutionRunning(false); + stop() + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + setExecutionModalView(0); + } + } + */ }; - + document.addEventListener('keydown', handleKeyDown); - + return () => { document.removeEventListener('keydown', handleKeyDown); } - }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]) + }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]) useEffect(() => { const handleKeyDown = (event) => { @@ -17127,147 +18754,185 @@ const releaseToConnectLabel = "Release to Connect" } } - if (( event.ctrlKey || event.metaKey ) && event.key === ";") { + if ((event.ctrlKey || event.metaKey) && event.key === ";") { if (!workflow.public && executionModalOpen) { getWorkflowExecution(props.match.params.key, ""); } } - /* - if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { - console.log("Shift key pressed") - if (!workflow.public && executionModalOpen) { - setExecutionRunning(false); - stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) - setExecutionModalView(0); - } - } - */ + /* + if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { + console.log("Shift key pressed") + if (!workflow.public && executionModalOpen) { + setExecutionRunning(false); + stop() + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + setExecutionModalView(0); + } + } + */ }; - + document.addEventListener('keydown', handleKeyDown); - + return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]); + }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]); - if (isMobile) { - return null - } + if (isMobile) { + return null + } - return ( -
    setHovered(true)} - onMouseLeave={() => setHovered(false)} - onClick={() => { - setExecutionModalOpen(true); - getWorkflowExecution(props.match.params.key, ""); - }} - > - - - Explore runs - -
    - ) + if (workflow.public === true) { + return null + } + + return ( +
    setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => { + setExecutionModalOpen(true); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) + }} + > + + + Explore runs + +
    + ) } // Used for handling suborg workflow distribution management const updateCurrentWorkflow = (inputworkflow) => { - //setLastSaved(false) - setSelectedAction({}); - setSelectedApp({}) - setWorkflow(inputworkflow) - if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { - getRevisionHistory(inputworkflow.id) - getWorkflowExecution(inputworkflow.id) - } + setCurrentWorkflow(inputworkflow) - // Update props match key - if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { - setDistributedFromParent(inputworkflow.parentorg_workflow) + //setLastSaved(false) + setSelectedAction({}); + setSelectedApp({}) + setWorkflow(inputworkflow) + + if (inputworkflow.id === originalWorkflow.id) { + if (selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null) { + setOriginalSelectedEnvironment(selectedActionEnvironment) + } + } + + if (inputworkflow.id === originalWorkflow.id && originalSelectedEnvironment !== undefined && originalSelectedEnvironment !== null && originalSelectedEnvironment.Name !== undefined && originalSelectedEnvironment.Name !== null) { + setSelectedActionEnvironment(originalSelectedEnvironment) } else { - setDistributedFromParent("") + //console.log("Checking input workflow actions for env: ", inputworkflow.actions) + if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { + + for (var actionkey in inputworkflow.actions) { + const action = inputworkflow.actions[actionkey] + if (action.environment === undefined || action.environment === null || action.environment === "") { + continue + } + + //const env = environments.find((a) => a.Name === action.environment) + const newenv = { + Name: action.environment, + Type: action.environment === "cloud" ? "cloud" : "onprem", + } + + setSelectedActionEnvironment(newenv) + break + } + } } - if (cy !== undefined) { - cy.removeListener("select"); - cy.removeListener("unselect"); + if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { + getRevisionHistory(inputworkflow.id, 50, 0, inputworkflow.org_id) + getWorkflowExecution(inputworkflow.id, "", executionFilter, inputworkflow.org_id) + loadTriggers(inputworkflow.org_id) + } - cy.removeListener("add"); - cy.removeListener("remove"); + // Update props match key + if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { + setDistributedFromParent(inputworkflow.parentorg_workflow) + } else { + setDistributedFromParent("") + } - cy.removeListener("mouseover"); - cy.removeListener("mouseout"); + if (cy !== undefined && cy !== null) { + cy.removeListener("select"); + cy.removeListener("unselect"); - cy.removeListener("drag"); - cy.removeListener("free"); - cy.removeListener("cxttap"); + cy.removeListener("add"); + cy.removeListener("remove"); - setElements([]) + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); - // Remove all edges - cy.edges().remove() - cy.nodes().remove() - } + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); + + + // Remove all edges + setElements([]) + cy.edges().remove() + cy.nodes().remove() + } } // Uses Org-Id referencing header to create a workflow while getting it in realtime // This further ensures the user needs access to GET the workflow properly const duplicateParentWorkflow = (inputWorkflow, org_id, setWorkflow) => { - fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { - method: "GET", - headers: { - "Org-Id": org_id, - "Content-Type": "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - getChildWorkflows(inputWorkflow.id) - } + fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { + method: "GET", + headers: { + "Org-Id": org_id, + "Content-Type": "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getChildWorkflows(inputWorkflow.id) + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === false) { - //toast("Failed to duplicate workflow") - } else { - //toast("Successfully duplicated workflow. Reloading child workflows.") - if (setWorkflow === true) { - updateCurrentWorkflow(responseJson) - } - } - }) - .catch((error) => { - console.log("Dupe workflow for suborg error: ", error.toString()) - }) + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + //toast("Failed to duplicate workflow") + } else { + //toast("Successfully duplicated workflow. Reloading child workflows.") + if (setWorkflow === true) { + updateCurrentWorkflow(responseJson) + } + } + }) + .catch((error) => { + console.log("Dupe workflow for suborg error: ", error.toString()) + }) } const BottomCytoscapeBar = () => { @@ -17275,13 +18940,13 @@ const releaseToConnectLabel = "Release to Connect" return null; } - const buttonHeights = 45 + const buttonHeights = 45 const boxSize = buttonHeights const executionButton = executionRunning ? ( - + + + ) return ( @@ -17326,32 +18991,32 @@ const releaseToConnectLabel = "Release to Connect" flexDirection: isMobile ? "column" : "row", }} > - - {executionButton} - - - + + {executionButton} + + + { setExecutionText(e.target.value); }} - // Start adornment + // Start adornment /> - + {/*userdata.avatar === creatorProfile.github_avatar ? null :*/} - - - - - - - {workflow.public || userdata.support == true ? - + maxHeight: buttonHeights, + }} + > + - : null} + {workflow.public || userdata.support == true ? + + + + + + : null} + + {/* */} - - - - - - - - + + + + + - - - - - - - - + removeNode(selectedNode.data("id")) + }} + > + + + + - {workflow.configuration !== null && - workflow.configuration !== undefined && - workflow.configuration.exit_on_error !== undefined ? ( - - ) : null} + + + + + - {/* + {workflow.configuration !== null && + workflow.configuration !== undefined && + workflow.configuration.exit_on_error !== undefined ? ( + + ) : null} + + {/* */} - - - - - - + + + + + + + -
    ); @@ -17651,12 +19316,12 @@ const releaseToConnectLabel = "Release to Connect" // defaultReturn = return null; } else { - /* - console.log( - "Unable to handle invalid trigger type " + - selectedTrigger.trigger_type - ); - */ + /* + console.log( + "Unable to handle invalid trigger type " + + selectedTrigger.trigger_type + ); + */ return null; } } else if (Object.getOwnPropertyNames(selectedEdge).length > 0) { @@ -17696,32 +19361,32 @@ const releaseToConnectLabel = "Release to Connect" {defaultReturn} : - - {/**/} + + {/**/}
    {defaultReturn}
    -
    +
    ); //return null; }; const unPublishWorkflow = (data) => { - data.id = props.match.params.key - if (!isCloud) { - toast("Function only supported on cloud") - return - } + data.id = props.match.params.key + if (!isCloud) { + toast("Function only supported on cloud") + return + } - if (data.public !== true) { - toast("Workflow is not public. Can't unpublish"); - return - } + if (data.public !== true) { + toast("Workflow is not public. Can't unpublish"); + return + } // This ALWAYS talks to Shuffle cloud data = JSON.parse(JSON.stringify(data)); - const url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/unpublish`; + const url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/unpublish`; fetch(url, { method: "POST", headers: { @@ -17731,27 +19396,27 @@ const releaseToConnectLabel = "Release to Connect" body: JSON.stringify(data), credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflow publish :O!"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflow publish :O!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.reason !== undefined) { - toast("Unpublishing: "+responseJson.reason) - } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.reason !== undefined) { + toast("Unpublishing: " + responseJson.reason) + } - if (responseJson.success === true) { - workflow.public = false - setWorkflow(workflow) - } - }) - .catch((error) => { - toast("Failed publishing: is the workflow valid? Remember to save the workflow first.") - console.log(error.toString()) - }) + if (responseJson.success === true) { + workflow.public = false + setWorkflow(workflow) + } + }) + .catch((error) => { + toast("Failed publishing: is the workflow valid? Remember to save the workflow first.") + console.log(error.toString()) + }) } // This can execute a workflow with firestore. Used for test, as datastore is old and stuff @@ -17767,20 +19432,20 @@ const releaseToConnectLabel = "Release to Connect" // console.log(allowList, userdata.public_username) const leftView = workflow.public === true ? -
    +
    - - - {workflow.name} - - {workflow.validated === true ? - - - - : null} - + + + {workflow.name} + + {workflow.validated === true ? + + + + : null} + This workflow is public and { saveWorkflow(workflow) @@ -17964,7 +19629,7 @@ const releaseToConnectLabel = "Release to Connect"
    : null} - {/* + {/*
    - {userdata.support === true ? - - - Manual Verification: {workflow.validated === undefined || workflow.validated === null || workflow.validated === false ? "Not valided" : "Validated"} - -
    - - Validate Workflow: - - { - workflow.validated = event.target.checked - workflow.user_editing = true - //setUserediting(true) + {userdata.support === true ? + + + Manual Verification: {workflow.validated === undefined || workflow.validated === null || workflow.validated === false ? "Not valided" : "Validated"} + +
    + + Validate Workflow: + + { + workflow.validated = event.target.checked + workflow.user_editing = true + //setUserediting(true) - saveWorkflow(workflow) - }} - /> -
    -
    - : null} + saveWorkflow(workflow) + }} + /> +
    +
    + : null}
    : null} @@ -18106,7 +19771,7 @@ const releaseToConnectLabel = "Release to Connect" marginBottom: 10, padding: 5, backgroundColor: theme.palette.backgroundColor, - borderRadius: theme.palette.borderRadius, + borderRadius: theme.palette.borderRadius, cursor: "pointer", display: "flex", minHeight: 45, @@ -18152,8 +19817,8 @@ const releaseToConnectLabel = "Release to Connect" setSelectedResult({ "action": { - "label": "Execution Argument", - "name": "Execution Argument", + "label": "Runtime Argument", + "name": "Runtime Argument", "large_image": theme.palette.defaultImage, "image": theme.palette.defaultImage, }, @@ -18178,12 +19843,12 @@ const releaseToConnectLabel = "Release to Connect" theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} collapsed={false} - shouldCollapse={(jsonField) => { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -18199,7 +19864,7 @@ const releaseToConnectLabel = "Release to Connect" return (
    -

    Execution Argument

    +

    Runtime Argument

    {executionData.execution_argument}
    @@ -18213,43 +19878,43 @@ const releaseToConnectLabel = "Release to Connect" const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" - const size = 40; - const borderRadius = 5 + const size = isCloud ? 40 : 35; + const borderRadius = 5 if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { return ( default ) } if (execution.execution_source === "authgroups") { - const iconMargin = 7 - return ( -
    - -
    + const iconMargin = 7 + return ( +
    + +
    ) - } else if (execution.execution_source === "webhook") { + } else if (execution.execution_source === "webhook") { return ( {"webhook"} trigger.trigger_type === "WEBHOOK") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "outlook") { @@ -18272,11 +19937,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "EMAIL") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "schedule") { @@ -18287,11 +19952,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "SCHEDULE") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "EMAIL") { @@ -18302,19 +19967,19 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "EMAIL") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); - } else if (execution.execution_source === "ShuffleGPT") { + } else if (execution.execution_source === "ShuffleGPT") { return ( - + ); } else if (execution.execution_source === "pipeline") { return ( @@ -18327,7 +19992,7 @@ const releaseToConnectLabel = "Release to Connect" style={{ width: size, height: size }} /> ); - } + } if ( execution.execution_parent !== null && @@ -18341,11 +20006,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "SUBFLOW") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } @@ -18354,11 +20019,11 @@ const releaseToConnectLabel = "Release to Connect" {execution.execution_source} ); }; @@ -18411,18 +20076,18 @@ const releaseToConnectLabel = "Release to Connect" base = JSON.stringify(base) } - if (base_node_name === "execution_argument" || base_node_name === "Execution Argument") { + if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") { base_node_name = "exec" } - console.log("COPY: ", base_node_name, copy); + //console.log("COPY: ", base_node_name, copy); //var newitem = JSON.parse(base); var newitem = validateJson(base).result // Check if base_node_name has changed if (cy !== undefined && cy !== null) { - console.log("Change name?") + //console.log("Change name?") //const allNodes = cy.nodes().jsons(); //for (var key in allNodes) { //const currentNode = allNodes[key]; @@ -18459,11 +20124,12 @@ const releaseToConnectLabel = "Release to Connect" } } - to_be_copied.replaceAll(" ", "_"); + to_be_copied = to_be_copied.replaceAll(" ", "_"); + console.log("COPY: ", to_be_copied); const elementName = "copy_element_shuffle"; var copyText = document.getElementById(elementName); if (copyText !== null && copyText !== undefined) { - console.log("NAVIGATOR: ", navigator); + //console.log("NAVIGATOR: ", navigator); const clipboard = navigator.clipboard; if (clipboard === undefined) { toast("Can only copy over HTTPS (port 3443)"); @@ -18476,7 +20142,7 @@ const releaseToConnectLabel = "Release to Connect" /* Copy the text inside the text field */ document.execCommand("copy"); - console.log("COPYING!"); + //console.log("COPYING!"); toast("Copied JSON path to clipboard.") } else { console.log("Couldn't find element ", elementName); @@ -18503,7 +20169,7 @@ const releaseToConnectLabel = "Release to Connect" console.log("IN useeffectt (2)" + collapsed) return; } - },[]) + }, []) /* componentWillUpdate = (nextProps, nextState) => { console.log(nextProps, nextState) @@ -18521,12 +20187,12 @@ const releaseToConnectLabel = "Release to Connect" theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} collapsed={parsedCollapse} - shouldCollapse={(jsonField) => { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -18537,8 +20203,6 @@ const releaseToConnectLabel = "Release to Connect" left: event.screenY, } - console.log("POS CLICK: ", pos) - setAnchorPosition(pos) }} onSelect={(select) => { @@ -18585,70 +20249,70 @@ const releaseToConnectLabel = "Release to Connect" ) } - const changeExecution = (data) => { - if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { - start() - setExecutionRunning(true) - setExecutionRequestStarted(false) - } + const changeExecution = (data) => { + if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { + start() + setExecutionRunning(true) + setExecutionRequestStarted(false) + } - var checkStarted = false - if (data.results !== undefined && data.results !== null && data.results.length > 0) { - if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - if (data.results !== undefined && data.results !== null) { - for (let resultkey in data.results) { - if (data.results[resultkey].status !== "SUCCESS") { - continue - } + var checkStarted = false + if (data.results !== undefined && data.results !== null && data.results.length > 0) { + if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + if (data.results !== undefined && data.results !== null) { + for (let resultkey in data.results) { + if (data.results[resultkey].status !== "SUCCESS") { + continue + } - if (data.results[resultkey].result.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - break - } - } - } - } - } + if (data.results[resultkey].result.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + break + } + } + } + } + } - const cur_execution = { - execution_id: data.execution_id, - authorization: data.authorization, - } + const cur_execution = { + execution_id: data.execution_id, + authorization: data.authorization, + } - setExecutionRequest(cur_execution) - setExecutionModalView(1) + setExecutionRequest(cur_execution) + setExecutionModalView(1) - if (!checkStarted) { - handleUpdateResults(data, cur_execution) + if (!checkStarted) { + handleUpdateResults(data, cur_execution) - if (cy !== undefined && cy !== null) { - cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); - for (let actionKey in data.workflow.actions) { - var actionitem = data.workflow.actions[actionKey] + if (cy !== undefined && cy !== null) { + cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); + for (let actionKey in data.workflow.actions) { + var actionitem = data.workflow.actions[actionKey] - handleColoring(actionitem.id, "", actionitem.label) - } + handleColoring(actionitem.id, "", actionitem.label) + } - for (let resultKey in data.results) { - var item = data.results[resultKey] + for (let resultKey in data.results) { + var item = data.results[resultKey] - handleColoring(item.action.id, item.status, item.action.label) - } - } + handleColoring(item.action.id, item.status, item.action.label) + } + } - setExecutionData(data) - } - } + setExecutionData(data) + } + } const ShowCopyingTooltip = () => { const [showCopying, setShowCopying] = React.useState(true) @@ -18682,14 +20346,14 @@ const releaseToConnectLabel = "Release to Connect" onClose={() => { setExecutionModalOpen(false) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + }} + style={{ + resize: "both", + overflow: "auto", }} - style={{ - resize: "both", - overflow: "auto", - }} hideBackdrop={false} variant="temporary" BackdropProps={{ @@ -18707,8 +20371,8 @@ const releaseToConnectLabel = "Release to Connect" fontSize: 18, borderLeft: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -18731,91 +20395,91 @@ const releaseToConnectLabel = "Release to Connect" : null} {executionModalView === 0 ? (
    -
    - -

    - - All Workflow Runs -

    -
    - - - - - -
    - - + + +
    + + + + { - getWorkflowExecution(props.match.params.key, "", executionFilter) - }} - color="secondary" - > - - Refresh Runs - - - + style={{ marginTop: 5, maxHeight: 50, overflow: "hidden", }}> > - - - - - + + + + +
    result.status === "SKIPPED").length - : 0 + data.results.filter((result) => result.status === "SKIPPED").length + : 0 const timestamp = new Date(data.started_at * 1000) .toLocaleString("en-GB") @@ -18854,25 +20518,27 @@ const releaseToConnectLabel = "Release to Connect" ? data.workflow.actions.length : 0; - if (data.workflow.triggers !== undefined && data.workflow.triggers !== null) { - for (let triggerkey in data.workflow.triggers) { - const trigger = data.workflow.triggers[triggerkey]; - if ( - (trigger.app_name === "User Input" && - trigger.trigger_type === "USERINPUT") || - (trigger.app_name === "Shuffle Workflow" && - trigger.trigger_type === "SUBFLOW") - ) { - calculatedResult += 1; - } - } - } + if (data.workflow.triggers !== undefined && data.workflow.triggers !== null) { + for (let triggerkey in data.workflow.triggers) { + const trigger = data.workflow.triggers[triggerkey]; + if ( + (trigger.app_name === "User Input" && + trigger.trigger_type === "USERINPUT") || + (trigger.app_name === "Shuffle Workflow" && + trigger.trigger_type === "SUBFLOW") + ) { + calculatedResult += 1; + } + } + } - const foundnotifications = data.notifications_created === undefined || data.notifications_created === null ? 0 : data.notifications_created + const foundnotifications = data.notifications_created === undefined || data.notifications_created === null ? 0 : data.notifications_created return ( - - {/**/} + + {/**/}
    1 Mb in cloud var checkStarted = false if (data.results !== undefined && data.results !== null && data.results.length > 0) { - if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - if (data.results !== undefined && data.results !== null) { - for (let resultkey in data.results) { - if (data.results[resultkey].status !== "SUCCESS") { - continue - } + if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + if (data.results !== undefined && data.results !== null) { + for (let resultkey in data.results) { + if (data.results[resultkey].status !== "SUCCESS") { + continue + } - if (data.results[resultkey].result.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - break - } - } - } - } - } + if (data.results[resultkey].result.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + break + } + } + } + } + } const cur_execution = { execution_id: data.execution_id, @@ -18937,20 +20603,20 @@ const releaseToConnectLabel = "Release to Connect" if (!checkStarted) { handleUpdateResults(data, cur_execution) - if (cy !== undefined && cy !== null) { - cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); - for (let actionKey in data.workflow.actions) { - var actionitem = data.workflow.actions[actionKey] + if (cy !== undefined && cy !== null) { + cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); + for (let actionKey in data.workflow.actions) { + var actionitem = data.workflow.actions[actionKey] - handleColoring(actionitem.id, "", actionitem.label) - } + handleColoring(actionitem.id, "", actionitem.label) + } - for (let resultKey in data.results) { - var item = data.results[resultKey] + for (let resultKey in data.results) { + var item = data.results[resultKey] - handleColoring(item.action.id, item.status, item.action.label) - } - } + handleColoring(item.action.id, item.status, item.action.label) + } + } setExecutionData(data) } @@ -18963,32 +20629,32 @@ const releaseToConnectLabel = "Release to Connect" width: lastExecution === data.execution_id ? 4 : 2, backgroundColor: statusColor, marginRight: 5, - maxHeight: 40, + maxHeight: 40, }} /> - 0 ? ` Authgroup: ${data.authgroup}` : '')} - placement="left" - > -
    - {getExecutionSourceImage(data)} -
    -
    + 0 ? ` Authgroup: ${data.authgroup}` : '')} + placement="left" + > +
    + {getExecutionSourceImage(data)} +
    +
    {timestamp} @@ -19002,29 +20668,30 @@ const releaseToConnectLabel = "Release to Connect"
    - {successActions} + {skippedActions > 0 ? skippedActions : {skippedActions}} = {calculatedResult} + {successActions} + {skippedActions > 0 ? skippedActions : {skippedActions}} = {calculatedResult}
    ) : null}
    - - {foundnotifications > 0 ? - - { - e.preventDefault() - e.stopPropagation() - window.open(`/admin?admin_tab=priorities&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") - }} - /> - - : null} + + {foundnotifications > 0 ? + + { + e.preventDefault() + e.stopPropagation() + window.open(`/admin?admin_tab=notifications&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") + }} + /> + + : null} {lastExecution === data.execution_id ? ( @@ -19044,84 +20711,85 @@ const releaseToConnectLabel = "Release to Connect"
    -
    +
    ); })}
    ) : ( - -
    - - No executions found for the '{executionFilter}' filter. - + +
    + + No executions found for the '{executionFilter}' filter. + - -
    -
    + +
    +
    )}
    ) : ( -
    +
    - - + { + setExecutionRunning(false); + stop(); + // getWorkflowExecution(currentWorkflow.id, ""); + getWorkflowExecution(props.match.params.key, ""); + setExecutionModalView(0); + setLastExecution(executionData.execution_id); + }} > - { + setExecutionRunning(false); + stop() + }} + > + + + +

    { - setExecutionRunning(false); - stop(); - getWorkflowExecution(props.match.params.key, ""); - setExecutionModalView(0); - setLastExecution(executionData.execution_id); - }} - > - { - setExecutionRunning(false); - stop() - }} - > - - - -

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

    -
    - - + > + See more runs +

    +
    +
    +
    -
    +

    Details

    { + const skip_popup = true executeWorkflow( executionData.execution_argument, executionData.start, - lastSaved + lastSaved, + skip_popup, ) if (executionText === undefined || executionText === null || executionText.length === 0) { @@ -19160,77 +20830,77 @@ const releaseToConnectLabel = "Release to Connect" - - - - - + changeExecution(data) + }} + > + + + + - - - - - + changeExecution(data) + }} + > + + + + {executionData.status === "EXECUTING" ? ( - ) : + ) : - { - e.preventDefault() - e.stopPropagation() - window.open(`/admin?admin_tab=priorities&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") - }} - /> + { + e.preventDefault() + e.stopPropagation() + window.open(`/admin?admin_tab=notifications&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") + }} + /> - } + } - {isCloud ? + {isCloud ? { - toast("Opening logs in a new tab") + toast("Opening logs in a new tab") - setTimeout(() => { - window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank") - }, 250) + setTimeout(() => { + window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank") + }, 250) }} > - + - : null} + : null}
    - {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 ? -
    - - - {/*envStatus === "success" ? - - - - : envStatus === "failure" ? - - - - : null*/} - - Env      - - - { - window.open("/admin?tab=locations", "_blank") - }}> - {executionData.workflow.actions[0].environment} - - -
    - : null} {executionData.status !== undefined && executionData.status.length > 0 ? (
    @@ -19347,7 +20992,7 @@ const releaseToConnectLabel = "Release to Connect" executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" || - (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? ( + (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? (
    Source    @@ -19355,53 +21000,53 @@ const releaseToConnectLabel = "Release to Connect" - {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? - - Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' - - : - executionData.execution_parent !== null && - executionData.execution_parent !== undefined && - executionData.execution_parent.length > 0 ? ( - executionData.execution_source === props.match.params.key ? - { - getWorkflowExecution( - props.match.params.key, - executionData.execution_parent - ); - }} - > - Parent Execution - - : - - Parent Workflow - + {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + + Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' + + : + executionData.execution_parent !== null && + executionData.execution_parent !== undefined && + executionData.execution_parent.length > 0 ? ( + executionData.execution_source === props.match.params.key ? + { + getWorkflowExecution( + props.match.params.key, + executionData.execution_parent + ); + }} + > + Parent Execution + + : + + Parent Workflow + ) - : - executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ? - - Form - - : - executionData.execution_source + : + executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ? + + Form + + : + executionData.execution_source }
    @@ -19432,27 +21077,53 @@ const releaseToConnectLabel = "Release to Connect" ) : null} - {userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" ? -
    - 0 ? +
    + - apps={apps} - workflow={executionData.workflow} - getParents={getParents} + {/*envStatus === "success" ? + + + + : envStatus === "failure" ? + + + + : null*/} - execution={executionData} - /> -
    - : null} + Location   + + + { + window.open("/admin?tab=locations", "_blank") + }}> + {executionData.workflow.actions[0].environment} + + +
    + : null} + + {userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" ? +
    + +
    + : null}
    - {executionData.execution_argument !== undefined && executionData.execution_argument !== null && + {executionData.execution_argument !== undefined && executionData.execution_argument !== null && executionData.execution_argument.length > 1 ? parsedExecutionArgument() - : - null} + : + null}
    {executionData.status !== undefined && - executionData.status !== "ABORTED" && - executionData.status !== "FINISHED" && - executionData.status !== "FAILURE" && - executionData.status !== "WAITING" && - !(executionData.results === undefined || executionData.results === null || (executionData.results.length === 0 && executionData.status === "EXECUTING")) ? ( + executionData.status !== "ABORTED" && + executionData.status !== "FINISHED" && + executionData.status !== "FAILURE" && + executionData.status !== "WAITING" && + !(executionData.results === undefined || executionData.results === null || (executionData.results.length === 0 && executionData.status === "EXECUTING")) ? (
    - { - console.log(environments, defaultEnvironmentIndex, nonskippedResults) - }} /> + { + console.log(environments, defaultEnvironmentIndex, nonskippedResults) + }} /> {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? @@ -19509,93 +21180,76 @@ const releaseToConnectLabel = "Release to Connect"
    { - executionData.results === undefined || - executionData.results === null || - (executionData.results.length === 0 && executionData.status === "EXECUTING") ? ( + executionData.results === undefined || + executionData.results === null || + (executionData.results.length === 0 && executionData.status === "EXECUTING") ? ( -
    - - {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? - - No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Learn more. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: support@shuffler.io - - : - null} -
    - ) : ( - executionData.results.map((data, index) => { - if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED")) { - return null; - } - - // FIXME: The latter replace doens't really work if ' is used in a string - var showResult = data.result.trim(); - const validate = validateJson(showResult); - - const curapp = apps.find( - (a) => - a.name === data.action.app_name && - a.app_version === data.action.app_version - ); - const imgsize = 50; - const statusColor = - data.status === "FINISHED" || data.status === "SUCCESS" - ? green - : data.status === "ABORTED" || data.status === "FAILURE" - ? "red" - : yellow; - - var imgSrc = curapp === undefined ? "" : curapp.large_image; - if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { - // Look for the node in the workflow - const action = workflow.actions.find( - (action) => action.id === data.action.id - ) - if (action !== undefined && action !== null) { - imgSrc = action.large_image; +
    + + {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? + + No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Learn more. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: support@shuffler.io + + : + null} +
    + ) : ( + executionData.results.map((data, index) => { + if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED")) { + return null; } - } - if ((imgSrc === undefined || imgSrc === null || imgSrc.length === 0) && cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(data.action.id) - if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { - // FIXME: Find image from cytoscape action - } else { - for (let actionkey in workflow.actions) { - if (workflow.actions[actionkey].app_name === data.action.app_name || workflow.actions[actionkey].id === data.action.id || workflow.actions[actionkey].label === data.action.label || workflow.actions[actionkey].name === data.action.name) { + // FIXME: The latter replace doens't really work if ' is used in a string + var showResult = data.result.trim(); + const validate = validateJson(showResult); - if (workflow.actions[actionkey].large_image !== undefined && workflow.actions[actionkey].large_image !== null && workflow.actions[actionkey].large_image.length > 0) { - imgSrc = workflow.actions[actionkey].large_image - break - } - } - } - } - } - - - var actionimg = - curapp === null ? null : ( - {data.action.app_name} + const curapp = apps.find( + (a) => + a.name === data.action.app_name && + a.app_version === data.action.app_version ); + const imgsize = 50; + const statusColor = + data.status === "FINISHED" || data.status === "SUCCESS" + ? green + : data.status === "ABORTED" || data.status === "FAILURE" + ? "red" + : yellow; - if (triggers.length > 2) { - if (data.action.app_name === "shuffle-subflow") { - const parsedImage = triggers[3].large_image; - actionimg = ( + var imgSrc = curapp === undefined ? "" : curapp.large_image; + if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { + // Look for the node in the workflow + const action = workflow.actions.find( + (action) => action.id === data.action.id + ) + if (action !== undefined && action !== null) { + imgSrc = action.large_image; + } + } + + if ((imgSrc === undefined || imgSrc === null || imgSrc.length === 0) && cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(data.action.id) + if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { + // FIXME: Find image from cytoscape action + } else { + for (let actionkey in workflow.actions) { + if (workflow.actions[actionkey].app_name === data.action.app_name || workflow.actions[actionkey].id === data.action.id || workflow.actions[actionkey].label === data.action.label || workflow.actions[actionkey].name === data.action.name) { + + if (workflow.actions[actionkey].large_image !== undefined && workflow.actions[actionkey].large_image !== null && workflow.actions[actionkey].large_image.length > 0) { + imgSrc = workflow.actions[actionkey].large_image + break + } + } + } + } + } + + + var actionimg = + curapp === null ? null : ( {"Shuffle ); - } - if (data.action.app_name === "User Input") { - actionimg = ( - {"Shuffle - ); - } - } + if (triggers.length > 2) { + if (data.action.app_name === "shuffle-subflow") { + const parsedImage = triggers[3].large_image; + actionimg = ( + {"Shuffle + ); + } - if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) { - const nodedata = cy.getElementById(data.action.id).data(); - //if (nodedata !== undefined && nodedata !== null && nodedata.fillstyle === "linear-gradient") { - if (nodedata !== undefined && nodedata !== null) { - var imgStyle = { - marginRight: 20, - width: imgsize, - height: imgsize, - border: `2px solid ${statusColor}`, - borderRadius: executionData.start === data.action.id ? 25 : 5, - background: `linear-gradient(to right, ${nodedata.fillGradient})`, - }; - - actionimg = ( - {nodedata.label} - ); - } else { - //console.log("Node not found: ", nodedata) - actionimg = ( - {data.action.app_name} - ) - } - } - - if (validate.valid && typeof validate.result === "string") { - validate.result = JSON.parse(validate.result); - } - - if (validate.valid && typeof validate.result === "object") { - if ( - validate.result.result !== undefined && - validate.result.result !== null - ) { - try { - validate.result.result = JSON.parse(validate.result.result); - } catch (e) { - //console.log("ERROR PARSING: ", e) + if (data.action.app_name === "User Input") { + actionimg = ( + {"Shuffle + ); } } - } + if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) { + const nodedata = cy.getElementById(data.action.id).data(); + //if (nodedata !== undefined && nodedata !== null && nodedata.fillstyle === "linear-gradient") { + if (nodedata !== undefined && nodedata !== null) { + var imgStyle = { + marginRight: 20, + width: imgsize, + height: imgsize, + border: `2px solid ${statusColor}`, + borderRadius: executionData.start === data.action.id ? 25 : 5, + background: `linear-gradient(to right, ${nodedata.fillGradient})`, + }; - var similarActionsView = null - if (data.similar_actions !== undefined && data.similar_actions !== null) { - var minimumMatch = 85 - var matching_executions = [] - if (data.similar_actions !== undefined && data.similar_actions !== null) { - for (let [k,kval] in Object.entries(data.similar_actions)){ - if (data.similar_actions.hasOwnProperty(k)) { - if (data.similar_actions[k].similarity > minimumMatch) { - matching_executions.push(data.similar_actions[k].execution_id) - } - } - } - } - - if (matching_executions.length !== 0) { - var parsed_url = matching_executions.join(",") - - similarActionsView = - - + ); + } else { + //console.log("Node not found: ", nodedata) + actionimg = ( + {data.action.app_name} { - navigate(`?execution_highlight=${parsed_url}`) - }} - > - - - + /> + ) + } } - } - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const chosenNodeId = new URLSearchParams(cursearch).get("node"); - const highlightNode = chosenNodeId !== null && chosenNodeId !== undefined && chosenNodeId !== "" && chosenNodeId === data.action.id + if (validate.valid && typeof validate.result === "string") { + validate.result = JSON.parse(validate.result); + } - return ( -
    { - if (cy == undefined || cy == null) { - return - } - - var currentnode = cy.getElementById(data.action.id); - if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) { - currentnode.addClass("shuffle-hover-highlight"); + if (validate.valid && typeof validate.result === "object") { + if ( + validate.result.result !== undefined && + validate.result.result !== null + ) { + try { + validate.result.result = JSON.parse(validate.result.result); + } catch (e) { + //console.log("ERROR PARSING: ", e) } + } + } - // Add a hover highlight - //var copyText = document.getElementById( - // "copy_element_shuffle" - //) - }} - onMouseOut={() => { - if (cy == undefined || cy == null) { - return - } - - var currentnode = cy.getElementById(data.action.id); - if (currentnode.length !== 0) { - currentnode.removeClass("shuffle-hover-highlight"); + var similarActionsView = null + if (data.similar_actions !== undefined && data.similar_actions !== null) { + var minimumMatch = 85 + var matching_executions = [] + if (data.similar_actions !== undefined && data.similar_actions !== null) { + for (let [k, kval] in Object.entries(data.similar_actions)) { + if (data.similar_actions.hasOwnProperty(k)) { + if (data.similar_actions[k].similarity > minimumMatch) { + matching_executions.push(data.similar_actions[k].execution_id) + } + } } - }} - > -
    -
    - { - if (cy !== undefined) { - const oldstartnode = cy.getElementById(data.action.id); - //console.log("FOUND NODe: ", oldstartnode) - if (oldstartnode !== undefined && oldstartnode !== null) { - const foundname = oldstartnode.data("label") - if (foundname !== undefined && foundname !== null) { - data.action.label = foundname - } - } + } - //console.log("Click data: ", data) - //data.action.label = "" - setSelectedResult(data); - setActiveDialog("result") - setCodeModalOpen(true); - } else { - toast("Please wait until the workflow is loaded and try again") - setCodeModalOpen(true) - setSelectedResult(data) + if (matching_executions.length !== 0) { + var parsed_url = matching_executions.join(",") - } - }} + similarActionsView = + - - - - - {actionimg} -
    -
    { + navigate(`?execution_highlight=${parsed_url}`) }} > - {data.action.label === undefined || data.action.label === null || data.action.label === "" ? data.action.label : data.action.label.replaceAll("_", " ")} - -
    -
    - - {data.action.name} - + + + + } + } + + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const chosenNodeId = new URLSearchParams(cursearch).get("node"); + const highlightNode = chosenNodeId !== null && chosenNodeId !== undefined && chosenNodeId !== "" && chosenNodeId === data.action.id + + return ( +
    { + if (cy == undefined || cy == null) { + return + } + + var currentnode = cy.getElementById(data.action.id); + if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) { + currentnode.addClass("shuffle-hover-highlight"); + } + + // Add a hover highlight + + //var copyText = document.getElementById( + // "copy_element_shuffle" + //) + }} + onMouseOut={() => { + if (cy == undefined || cy == null) { + return + } + + var currentnode = cy.getElementById(data.action.id); + if (currentnode.length !== 0) { + currentnode.removeClass("shuffle-hover-highlight"); + } + }} + > +
    +
    + { + if (cy !== undefined && cy !== null) { + const oldstartnode = cy.getElementById(data.action.id); + //console.log("FOUND NODe: ", oldstartnode) + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + data.action.label = foundname + } + } + + //console.log("Click data: ", data) + //data.action.label = "" + setSelectedResult(data); + setActiveDialog("result") + setCodeModalOpen(true); + } else { + toast("Please wait until the workflow is loaded and try again") + setCodeModalOpen(true) + setSelectedResult(data) + + } + }} + > + + + + + {actionimg} +
    +
    + {data.action.label === undefined || data.action.label === null || data.action.label === "" ? data.action.label : data.action.label.replaceAll("_", " ")} + +
    +
    + + {data.action.name} + +
    -
    - {data.action.app_name === "shuffle-subflow" && - validate.result.success !== undefined && - validate.result.success === true ? ( - - {validate.valid && - data.action.parameters !== undefined && - data.action.parameters !== null && - data.action.parameters.length > 0 ? ( - data.action.parameters[0].value === - props.match.params.key ? ( - { - getWorkflowExecution( - props.match.params.key, - validate.result.execution_id - ); - }} - > - - + {data.action.app_name === "shuffle-subflow" && + validate.result.success !== undefined && + validate.result.success === true ? ( + + {validate.valid && + data.action.parameters !== undefined && + data.action.parameters !== null && + data.action.parameters.length > 0 ? ( + data.action.parameters[0].value === + props.match.params.key ? ( + { + getWorkflowExecution( + props.match.params.key, + validate.result.execution_id + ); + }} + > + + + ) : ( + { }} + > + + + ) ) : ( - { }} - > - - - ) - ) : ( - "" - )} - - ) : null} -
    - - { data.status !== "SUCCESS" ? -
    - - Status  - - - {data.status} - - {similarActionsView} -
    - : null} - - {validate.valid ? ( - - { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} - enableClipboard={(copy) => { - handleReactJsonClipboard(copy); - }} - displayDataTypes={false} - onSelect={(select) => { - HandleJsonCopy(showResult, select, data.action.label); - console.log("SELECTED!: ", select); - }} - name={"Results for " + data.action.label} - /> - - - ) : ( -
    - - Result  - - - {data.result} - + "" + )} + + ) : null}
    - )} -
    - ); - }) - )} + + {data.status !== "SUCCESS" ? +
    + + Status  + + + {data.status} + + {similarActionsView} +
    + : null} + + {validate.valid ? ( + + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + enableClipboard={(copy) => { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(showResult, select, data.action.label); + console.log("SELECTED!: ", select); + }} + name={"Results for " + data.action.label} + /> + + + ) : ( +
    + + Result  + + + {data.result} + +
    + )} +
    + ); + }) + )}
    )} @@ -19974,45 +21645,45 @@ const releaseToConnectLabel = "Release to Connect" const [open, setOpen] = React.useState(false) const showVariable = data.value.length < 60 - // Check if it's valid JSON - const checked = validateJson(data.value.trim()) + // Check if it's valid JSON + const checked = validateJson(data.value.trim()) - if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { - return ( -
    - - Action Logs - - - Logs for an action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. - -
    - ) - } + if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { + return ( +
    + + Action Logs + + + More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. + +
    + ) + } - var showlink = false - if (data.name.endsWith("-Url")) { - //data.name = data.name.toLowerCase().replaceAll("-", "_") - if (data.value.startsWith(", ")) { - data.value = data.value.substring(2) - } + var showlink = false + if (data.name.endsWith("-Url")) { + //data.name = data.name.toLowerCase().replaceAll("-", "_") + if (data.value.startsWith(", ")) { + data.value = data.value.substring(2) + } - if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) { - showlink = true - } - } + if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) { + showlink = true + } + } return (
    {data.value.length > 60 || checked.valid ? {data.name} - {checked.valid ? - - : null} - {showVariable ? data.value : null} + {checked.valid ? + + : null} + {showVariable ? data.value : null} : @@ -20048,40 +21719,40 @@ const releaseToConnectLabel = "Release to Connect" } {open ? - checked.valid ? - { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} - displayDataTypes={false} - name={"Parsed data for variable " + data.name} - /> - : - { - if (showlink) { - e.preventDefault() - e.stopPropagation() - window.open(data.value, "_blank") - } - }} - color={showlink ? "inherit" : "textSecondary"} - > - {data.value} - + checked.valid ? + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + displayDataTypes={false} + name={"Parsed data for variable " + data.name} + /> + : + { + if (showlink) { + e.preventDefault() + e.stopPropagation() + window.open(data.value, "_blank") + } + }} + color={showlink ? "inherit" : "textSecondary"} + > + {data.value} + : null}
    ) @@ -20091,258 +21762,258 @@ const releaseToConnectLabel = "Release to Connect" // Should probably put this on the backend instead when notifications are made :)) const getErrorSuggestion = (result) => { - if (result === undefined || result === null) { - return "" - } + if (result === undefined || result === null) { + return "" + } - // Check if array with json inside to handle one item at a time~ - if (typeof result === "object" && result.length !== undefined) { - if (result.length > 0) { - // Check type inside - if (typeof result[0] === "object") { - result = result[0] - } - } - } + // Check if array with json inside to handle one item at a time~ + if (typeof result === "object" && result.length !== undefined) { + if (result.length > 0) { + // Check type inside + if (typeof result[0] === "object") { + result = result[0] + } + } + } - if (result.success === true && result.status === 200) { - if (result.body !== undefined && result.body !== null) { - const stringbody = result.body.toString() - if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) { - return "" - } + if (result.success === true && result.status === 200) { + if (result.body !== undefined && result.body !== null) { + const stringbody = result.body.toString() + if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) { + return "" + } - if (stringbody.length > 1000) { - return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file." - } - } - } + if (stringbody.length > 1000) { + return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file." + } + } + } - if (result.status === 429) { - return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again." - } + if (result.status === 429) { + return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again." + } - if (result.status === 405) { - return "Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to support@shuffler.io" - } + if (result.status === 405) { + return "Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to support@shuffler.io" + } - if (result.status === 415) { - return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow." - } + if (result.status === 415) { + return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow." + } - if (result.status === 401) { - return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information." - } + if (result.status === 401) { + return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information." + } - if (result.status === 403) { - return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information." - } + if (result.status === 403) { + return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information." + } - if (result.status === 404) { - return "The URL, or content of the URL is incorrect. Check it and try again." - } + if (result.status === 404) { + return "The URL, or content of the URL is incorrect. Check it and try again." + } - if (result.status === 400) { - return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information." - } + if (result.status === 400) { + return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information." + } - if (result.status === 200 || result.status === 201 || result.status === 204) { - return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct." - } + if (result.status === 200 || result.status === 201 || result.status === 204) { + return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct." + } - // Validate and check for newlines - if (result.success !== false) { + // Validate and check for newlines + if (result.success !== false) { - var stringjson = result - const valid = validateJson(stringjson, true) - if (valid.valid === false) { - if (stringjson.startsWith("{") && stringjson.endsWith("}")) { - // Look for newline - if (stringjson.includes("\n") && !stringjson.includes("\n")) { - return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid." - } else { - return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines" - } - } - } + var stringjson = result + const valid = validateJson(stringjson, true) + if (valid.valid === false) { + if (stringjson.startsWith("{") && stringjson.endsWith("}")) { + // Look for newline + if (stringjson.includes("\n") && !stringjson.includes("\n")) { + return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid." + } else { + return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines" + } + } + } - //return "" - } + //return "" + } - try { - stringjson = JSON.stringify(result) - } catch (e) { - } + try { + stringjson = JSON.stringify(result) + } catch (e) { + } - stringjson = stringjson.toLowerCase() - if (stringjson.includes("localhost")) { - return "You can't use localhost in apps. Use the external ip or url of the server instead" - } + stringjson = stringjson.toLowerCase() + if (stringjson.includes("localhost")) { + return "You can't use localhost in apps. Use the external ip or url of the server instead" + } - if (stringjson.includes("manifest unknown")) { - return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support" - } + if (stringjson.includes("manifest unknown")) { + return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support" + } - if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) { - return "Consider whether your Orborus environment can connect to a local IP or not." - } + if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) { + return "Consider whether your Orborus environment can connect to a local IP or not." + } - if (stringjson.includes("kms/")) { - return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=priorities. If you need help with KMS, please contact support@shuffler.io" - } + if (stringjson.includes("kms/")) { + return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io" + } - if (stringjson.includes("invalidurl")) { - // IF count of "http" is more than one, 1, it's prolly invalid - var additionalinfo = "" - if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) { - additionalinfo = "You may be using multiple 'http' in the URL. " - } + if (stringjson.includes("invalidurl")) { + // IF count of "http" is more than one, 1, it's prolly invalid + var additionalinfo = "" + if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) { + additionalinfo = "You may be using multiple 'http' in the URL. " + } - return "The URL is invalid. Change the URL to a valid one, and try again. "+additionalinfo - } + return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo + } - if (stringjson.includes("result too large to handle")) { - return "Execution loading failed. Reload the execution by closing it and clicking it again" - } + if (stringjson.includes("result too large to handle")) { + return "Execution loading failed. Reload the execution by closing it and clicking it again" + } - if (isCloud && stringjson.toLowerCase().includes("timeout error")) { - return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locationsto create an environment to connect to" - } + if (isCloud && stringjson.toLowerCase().includes("timeout error")) { + return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to" + } - if (stringjson.toLowerCase().includes("invalid header")) { - return "A header or authentication token in the app is invalid. Check the app's configuration" - } + if (stringjson.toLowerCase().includes("invalid header")) { + return "A header or authentication token in the app is invalid. Check the app's configuration" + } - if (stringjson.includes("connectionerror")) { - if (stringjson.includes("kms")) { - return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=priorities&kms=true. If you need help with KMS, please contact support@shuffler.io" - } + if (stringjson.includes("connectionerror")) { + if (stringjson.includes("kms")) { + return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io" + } - return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." - } + return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." + } - return "" + return "" } const currentSuggestion = getErrorSuggestion(validate.result) const codePopoutModal = !codeModalOpen ? null : ( - setActiveDialog("result"), - style: { - pointerEvents: "auto", - color: "white", - minWidth: isMobile ? "90%" : 750, - padding: 30, - maxHeight: 550, - overflowY: "auto", - overflowX: "hidden", - border: theme.palette.defaultBorder, + setActiveDialog("result"), + style: { + pointerEvents: "auto", + color: "white", + minWidth: isMobile ? "90%" : 750, + padding: 30, + maxHeight: 550, + overflowY: "auto", + overflowX: "hidden", + border: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", - }, - }} - > - {/* Have a sticky top bar */} - - + {/* Have a sticky top bar */} + + + { + e.preventDefault() + }} > - { - e.preventDefault() - }} - > - - - - - { - e.preventDefault() + + + + + { + e.preventDefault() - if (workflowExecutions !== null) { - for (let execkey in workflowExecutions) { - const execution = workflowExecutions[execkey]; - if (execution.execution_argument.includes("too large")) { - continue - } - - const result = execution.results.find((data) => data.status === "SUCCESS" && data.action.id === selectedResult.action.id) - - if (result !== undefined) { - const oldstartnode = cy.getElementById(selectedResult.action.id) - if (oldstartnode !== undefined && oldstartnode !== null) { - const foundname = oldstartnode.data("label") - if (foundname !== undefined && foundname !== null) { - result.action.label = foundname - } - } - - setSelectedResult(result) - setUpdate(Math.random()) - break; - } - } - } - }} - > - - - - - { - e.preventDefault(); + if (workflowExecutions !== null) { for (let execkey in workflowExecutions) { const execution = workflowExecutions[execkey]; - const result = execution.results.find( - (data) => - data.action.id === selectedResult.action.id && - data.status !== "SUCCESS" && - data.status !== "SKIPPED" && - data.status !== "WAITING" - ); + if (execution.execution_argument.includes("too large")) { + continue + } + + const result = execution.results.find((data) => data.status === "SUCCESS" && data.action.id === selectedResult.action.id) + + if (result !== undefined) { + const oldstartnode = cy.getElementById(selectedResult.action.id) + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + result.action.label = foundname + } + } + + setSelectedResult(result) + setUpdate(Math.random()) + break; + } + } + } + }} + > + + + + + { + e.preventDefault(); + for (let execkey in workflowExecutions) { + const execution = workflowExecutions[execkey]; + const result = execution.results.find( + (data) => + data.action.id === selectedResult.action.id && + data.status !== "SUCCESS" && + data.status !== "SKIPPED" && + data.status !== "WAITING" + ); if (result !== undefined) { const oldstartnode = cy.getElementById(selectedResult.action.id); @@ -20378,15 +22049,15 @@ const releaseToConnectLabel = "Release to Connect" setExecutionModalOpen(true); setExecutionModalView(1); - if (workflowExecutions[executionIndex] !== undefined && workflowExecutions[executionIndex] !== null && workflowExecutions[executionIndex].execution_argument.includes("too large")) { - //checkStarted = true - setExecutionData({}); - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - setExecutionData(workflowExecutions[executionIndex]); - } + if (workflowExecutions[executionIndex] !== undefined && workflowExecutions[executionIndex] !== null && workflowExecutions[executionIndex].execution_argument.includes("too large")) { + //checkStarted = true + setExecutionData({}); + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + setExecutionData(workflowExecutions[executionIndex]); + } } }} > @@ -20400,13 +22071,13 @@ const releaseToConnectLabel = "Release to Connect" > { }} > @@ -20441,8 +22112,8 @@ const releaseToConnectLabel = "Release to Connect" width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, - filter: curapp === undefined ? "grayscale(100%)" : null, - borderRadius: theme.palette?.borderRadius, + filter: curapp === undefined ? "grayscale(100%)" : null, + borderRadius: theme.palette?.borderRadius, }} /> )} @@ -20464,15 +22135,15 @@ const releaseToConnectLabel = "Release to Connect"
    - {currentSuggestion.length > 0 ? -
    - Debug: {currentSuggestion} -
    - : -
    - Status {selectedResult.status} -
    - } + {currentSuggestion.length > 0 ? +
    + Debug: {currentSuggestion} +
    + : +
    + Status {selectedResult.status} +
    + } {validate.valid ? ( { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -20498,13 +22169,13 @@ const releaseToConnectLabel = "Release to Connect" ) : (
    Result -
    +
    { to_be_copied = selectedResult.result; @@ -20555,9 +22226,9 @@ const releaseToConnectLabel = "Release to Connect" variant="h6" style={{ marginBottom: 0, marginTop: 0 }} > - Variables (click to expand) + Variable & Debug info ({selectedResult?.action?.parameters?.length}) - {selectedResult.action.parameters.map((data, index) => { + {selectedResult?.action?.parameters?.map((data, index) => { if (data.value.length === 0) { return null; } @@ -20582,7 +22253,7 @@ const releaseToConnectLabel = "Release to Connect" ); const newView = ( -
    +
    @@ -20596,29 +22267,41 @@ const releaseToConnectLabel = "Release to Connect" textAlign: "center", }} > - - - Loading Workflow - + {isLoaded && workflowDone ? +
    + {/* + + No workflow to load. Workflow runs may still exist. If you think this is wrong, please contact support@shuffler.io + + */} +
    + : +
    + + + Loading Workflow + +
    + }
    ) : ( - - {/**/} + + {/**/} { // FIXME: There's something specific loading when // you do the first hover of a node. Why is this different? - - setCy(incy); + + setCy(incy); }} /> - + )}
    {executionModal} - + { - rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ? - -
    - -
    -
    - : null - } - {/* Looks for triggers" */} - {/* Only fixed the ones that require scrolling on a small screen */} - {/* Most important: Actions. But these are a lot more complex */} - {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT") ? -
    - {Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ? - selectedTrigger.trigger_type === "SCHEDULE" ? - ScheduleSidebar - : selectedTrigger.trigger_type === "PIPELINE" ? - PipelineSidebar - : selectedTrigger.trigger_type === "WEBHOOK" ? - WebhookSidebar - : selectedTrigger.trigger_type === "USERINPUT" ? - UserinputSidebar - : null - : null} -
    - : null} + rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ? + +
    + +
    +
    + : null + } + {/* Looks for triggers" */} + {/* Only fixed the ones that require scrolling on a small screen */} + {/* Most important: Actions. But these are a lot more complex */} + {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT" || selectedTrigger.trigger_type === "SUBFLOW") ? +
    + {Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ? + selectedTrigger.trigger_type === "SCHEDULE" ? + ScheduleSidebar + : selectedTrigger.trigger_type === "PIPELINE" ? + PipelineSidebar + : selectedTrigger.trigger_type === "WEBHOOK" ? + WebhookSidebar + : selectedTrigger.trigger_type === "USERINPUT" ? + UserinputSidebar + : selectedTrigger.trigger_type === "SUBFLOW" ? + SubflowSidebar + : null + : null} +
    + : null} + {/* { rightSideBarOpen && selectedTrigger?.trigger_type === "SUBFLOW"&& Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ?
    : null - } - - {/* + } */} + + {/* */} - {showWorkflowRevisions ? null : - - {/**/} - {shownErrors} - - - - - } + {showWorkflowRevisions ? null : + + {/**/} + {shownErrors} + + + + + }
    ); @@ -20807,10 +22496,10 @@ const releaseToConnectLabel = "Release to Connect" color: "white", border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : 800, - minWidth: isMobile ? bodyWidth - 100 : 800, + minWidth: isMobile ? bodyWidth - 100 : 800, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -20840,7 +22529,7 @@ const releaseToConnectLabel = "Release to Connect" }, }} margin="dense" - label="Name" + label="Name" fullWidth defaultValue={newVariableName} /> @@ -20855,7 +22544,7 @@ const releaseToConnectLabel = "Release to Connect" }, }} margin="dense" - label="Default Value (optional)" + label="Default Value (optional)" fullWidth defaultValue={newVariableValue} /> @@ -20877,7 +22566,6 @@ const releaseToConnectLabel = "Release to Connect" disabled={newVariableName.length === 0} variant="contained" onClick={() => { - console.log("VARIABLES! ", newVariableName); if ( workflow.execution_variables === undefined || workflow.execution_variables === null @@ -20895,9 +22583,9 @@ const releaseToConnectLabel = "Release to Connect" workflow.execution_variables[found].name = newVariableName; } - if (newVariableValue.length > 0) { - workflow.execution_variables[found].value = newVariableValue; - } + if (newVariableValue.length > 0) { + workflow.execution_variables[found].value = newVariableValue; + } } else { workflow.execution_variables.push({ name: newVariableName, @@ -20988,8 +22676,8 @@ const releaseToConnectLabel = "Release to Connect" border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : "100%", - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -21148,13 +22836,16 @@ const releaseToConnectLabel = "Release to Connect" selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0 ) { - return ( + return null + /* + ( {selectedApp.name} does not require authentication ); + */ } authenticationOption.app.actions = []; @@ -21214,18 +22905,18 @@ const releaseToConnectLabel = "Release to Connect" selectedAction.authentication_id = authenticationOption.id; selectedAction.selectedAuthentication = authenticationOption; - console.log("auth option 4: ", authenticationOption) + console.log("auth option 4: ", authenticationOption) if (selectedAction.authentication === undefined || selectedAction.authentication === null) { selectedAction.authentication = [authenticationOption] } else { - try { - selectedAction.authentication.push(authenticationOption) - } catch (e) { - //console.log("Error: ", e) - } + try { + selectedAction.authentication.push(authenticationOption) + } catch (e) { + //console.log("Error: ", e) + } } setSelectedAction(selectedAction) @@ -21250,8 +22941,8 @@ const releaseToConnectLabel = "Release to Connect" setUpdate(authenticationOption.id) } - if (authenticationOption.label === null || authenticationOption.label === undefined) { - authenticationOption.label = selectedApp.name + " authentication"; + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; } return ( @@ -21300,35 +22991,35 @@ const releaseToConnectLabel = "Release to Connect" />
    {selectedApp.authentication.parameters.map((data, index) => { - // FIXME: Look for relevant fields in the action that may already be filled in with the same name - if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { - if (selectedAction !== undefined && selectedAction !== null && selectedAction.parameters !== undefined && selectedAction.parameters !== null) { - for (var fieldkey in selectedAction.parameters) { - const field = selectedAction.parameters[fieldkey] - if (field.name !== data.name) { - continue - } + // FIXME: Look for relevant fields in the action that may already be filled in with the same name + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + if (selectedAction !== undefined && selectedAction !== null && selectedAction.parameters !== undefined && selectedAction.parameters !== null) { + for (var fieldkey in selectedAction.parameters) { + const field = selectedAction.parameters[fieldkey] + if (field.name !== data.name) { + continue + } - if (field.value !== undefined && field.value !== null && field.value.length > 0) { - data.value = field.value - data.autocomplete = true - break - } - } - } - } + if (field.value !== undefined && field.value !== null && field.value.length > 0) { + data.value = field.value + data.autocomplete = true + break + } + } + } + } return (
    -
    - - - {data?.name?.endsWith("_basic") ? data?.name?.replace("_basic", "") : data?.name} - -
    +
    + + + {data?.name?.endsWith("_basic") ? data?.name?.replace("_basic", "") : data?.name} + +
    {data.schema !== undefined && data.schema !== null && @@ -21399,7 +23090,7 @@ const releaseToConnectLabel = "Release to Connect" authenticationOption.fields[data.name] = event.target.value; }} - id={`${data.name}_auth`} + id={`${data.name}_auth`} /> )}
    @@ -21408,9 +23099,9 @@ const releaseToConnectLabel = "Release to Connect"
    - - + - - - + cursor: "move", + }} + > + + +
    - {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? + {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? - : + : }
    @@ -21647,51 +23342,51 @@ const releaseToConnectLabel = "Release to Connect" overflowY: "auto", overflowX: "hidden", }} - onLoad={() => { - /* - if (isCloud && ReactGA !== undefined) { - toast("Sending GA info") - // Google analytics info about what app people are looking at - ReactGA.event({ - category: "workflow", - action: `documentation_load`, - label: selectedApp.name, - }) - - } - */ - }} + onLoad={() => { + /* + if (isCloud && ReactGA !== undefined) { + toast("Sending GA info") + // Google analytics info about what app people are looking at + ReactGA.event({ + category: "workflow", + action: `documentation_load`, + label: selectedApp.name, + }) + + } + */ + }} > {selectedApp.documentation === undefined || selectedApp.documentation === null || selectedApp.documentation.length === 0 ? ( - -
    - - {selectedApp.description} - -
    + +
    + + {selectedApp.description} + +
    -
    - - There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! - - -
    + setTimeout(() => { + window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedApp.name.toLowerCase()}.md`, "_blank") + }, 2500) + }} + > +   Create Docs + +
    Want to help the making of, or improve this app?{" "} -
    +
    ) : ( -
    - {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? -
    -
    - {isMobile ? null : ( - - - - - - )} - {isMobile ? null : ( -
    - )} - - {selectedMeta.read_time} minute - {selectedMeta.read_time === 1 ? "" : "s"} to read - -
    -
    - {isMobile || - selectedMeta.contributors === undefined || - selectedMeta.contributors === null ? ( - "" - ) : ( -
    - {selectedMeta.contributors.slice(0, 7).map((data, index) => { - return ( - - - {data.url} - - - ); - })} -
    - )} -
    -
    - : null} +
    + {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
    +
    + {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    +
    + {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + : null} - - {selectedApp.documentation} - -
    + + {selectedApp.documentation} + +
    )}
    ) : null; - const tenzirConfigModal = !tenzirConfigModalOpen ? null : - - -
    Run a Tenzir Pipeline
    - - Runs a Tenzir pipeline. You can use the output of the pipeline in your workflow. - -
    - -
    - Pipeline - -
    - -
    - - - - -
    + placeholder={""} + defaultValue={selectedOption} + /> +
    - const SuggestionBoxUi = () => { - const [suggestionValue, setSuggestionValue] = useState(""); - const [suggestionLoading, setSuggestionLoading] = useState(false); - const [responseMsg, setResponseMsg] = useState(""); + - if (suggestionBox === undefined || suggestionBox.open === false) { - return false - } + + + + - return ( -
    - {/* + const SuggestionBoxUi = () => { + const [suggestionValue, setSuggestionValue] = useState(""); + const [suggestionLoading, setSuggestionLoading] = useState(false); + const [responseMsg, setResponseMsg] = useState(""); + + if (suggestionBox === undefined || suggestionBox.open === false) { + return false + } + + return ( +
    + {/* */} - - { - e.preventDefault(); - setSuggestionBox({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "value": "", - "loading": false, - }); - }} - > - - - - { - e.preventDefault(); - aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) - }}> - { - setSuggestionValue(e.target.value) - }} - InputProps={{ - endAdornment: ( - - - { - e.preventDefault(); - aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) - }} /> - - - ), - }} - /> - - {suggestionLoading === true ? - - : null} - {responseMsg.length > 0 ? - - {responseMsg} - - : null} -
    - ) - } + + { + e.preventDefault(); + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "value": "", + "loading": false, + }); + }} + > + + + +
    { + e.preventDefault(); + aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) + }}> + { + setSuggestionValue(e.target.value) + }} + InputProps={{ + endAdornment: ( + + + { + e.preventDefault(); + aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) + }} /> + + + ), + }} + /> + + {suggestionLoading === true ? + + : null} + {responseMsg.length > 0 ? + + {responseMsg} + + : null} +
    + ) + } - - /*else if (selectedRevision === undefined || selectedRevision === null || selectedRevision == {} && originalWorkflow !== undefined && originalWorkflow !== null && originalWorkflow !== {}) { - console.log("Setting original workflow as selected revision") - setSelectedRevision(originalWorkflow) - }*/ - const RevisionBox = (props) => { - const { revision, showBorder, } = props - if (revision === undefined || revision === null) { - return null - } + /*else if (selectedRevision === undefined || selectedRevision === null || selectedRevision == {} && originalWorkflow !== undefined && originalWorkflow !== null && originalWorkflow !== {}) { + console.log("Setting original workflow as selected revision") + setSelectedRevision(originalWorkflow) + }*/ - var newrevision = JSON.parse(JSON.stringify(revision)) - // Make unix timestamp into ISO timestamp in the format July 27th, 3:05 AM - // Format: July 27th, 3:05 AM - //console.log("Edited time: ", revision.edited) - // Convert 1692128391 to valid timestamp - const validTimestamp = newrevision.edited.toString().length === 10 ? newrevision.edited * 1000 : newrevision.edited - const translatedDate = new Date(validTimestamp).toLocaleString('en-US', { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true }) + const RevisionBox = (props) => { + const { revision, showBorder, } = props + if (revision === undefined || revision === null) { + return null + } - var workflowStatus = newrevision.status !== undefined && newrevision.status !== null && newrevision.status !== "" ? newrevision.status : "test" - if (newrevision.name !== undefined && newrevision.name !== null && newrevision.name !== "") { - if (newrevision.name.toLowerCase().includes("test")) { - workflowStatus = "test" - } + var newrevision = JSON.parse(JSON.stringify(revision)) + // Make unix timestamp into ISO timestamp in the format July 27th, 3:05 AM + // Format: July 27th, 3:05 AM + //console.log("Edited time: ", revision.edited) + // Convert 1692128391 to valid timestamp + const validTimestamp = newrevision.edited.toString().length === 10 ? newrevision.edited * 1000 : newrevision.edited + const translatedDate = new Date(validTimestamp).toLocaleString('en-US', { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true }) - if (newrevision.name.toLowerCase().includes("dev") || newrevision.name.toLowerCase().includes("staging") || newrevision.name.toLowerCase().includes("rollback")) { - workflowStatus = "dev" - } + var workflowStatus = newrevision.status !== undefined && newrevision.status !== null && newrevision.status !== "" ? newrevision.status : "test" + if (newrevision.name !== undefined && newrevision.name !== null && newrevision.name !== "") { + if (newrevision.name.toLowerCase().includes("test")) { + workflowStatus = "test" + } - if (newrevision.name.toLowerCase().includes("prod") || newrevision.name.toLowerCase().includes("main")) { - workflowStatus = "prod" - } - } + if (newrevision.name.toLowerCase().includes("dev") || newrevision.name.toLowerCase().includes("staging") || newrevision.name.toLowerCase().includes("rollback")) { + workflowStatus = "dev" + } - return ( - { + if (newrevision.name.toLowerCase().includes("prod") || newrevision.name.toLowerCase().includes("main")) { + workflowStatus = "prod" + } + } + + return ( + { setRightSideBarOpen(false) - if (newrevision.edited === selectedVersion.edited) { - console.log("Same revision! No setting.") - return - } + if (newrevision.edited === selectedVersion.edited) { + console.log("Same revision! No setting.") + return + } - // Should render if it's not the same as workflow.edited - console.log("Clicked revision: ", newrevision) - setLastSaved(false) - setSelectedVersion(newrevision); - setWorkflow(newrevision) - setSelectedAction({}); - setSelectedApp({}) + // Should render if it's not the same as workflow.edited + console.log("Clicked revision: ", newrevision) + setLastSaved(false) + setSelectedVersion(newrevision); + setWorkflow(newrevision) + setSelectedAction({}); + setSelectedApp({}) - // Remove all cytoscape triggers first? - if (cy !== undefined && cy !== null) { - cy.removeListener("select"); - cy.removeListener("unselect"); + // Remove all cytoscape triggers first? + if (cy !== undefined && cy !== null) { + cy.removeListener("select"); + cy.removeListener("unselect"); - cy.removeListener("add"); - cy.removeListener("remove"); + cy.removeListener("add"); + cy.removeListener("remove"); - cy.removeListener("mouseover"); - cy.removeListener("mouseout"); + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); - cy.removeListener("drag"); - cy.removeListener("free"); - cy.removeListener("cxttap"); + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); - setElements([]) + setElements([]) - cy.remove('*') - cy.edges().remove() - cy.nodes().remove() - } + cy.remove('*') + cy.edges().remove() + cy.nodes().remove() + } - // Remove all cy nodes - setTimeout(() => { - //toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length) - setupGraph(newrevision) - }, 250) + // Remove all cy nodes + setTimeout(() => { + //toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length) + setupGraph(newrevision) + }, 250) - // Re-adding cytoscape triggers - if (cy !== undefined && cy !== null) { - cy.on("select", "node", (e) => { - onNodeSelect(e, appAuthentication); - }); - cy.on("select", "edge", (e) => onEdgeSelect(e)); + // Re-adding cytoscape triggers + if (cy !== undefined && cy !== null) { + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); - cy.on("unselect", (e) => onUnselect(e)); + cy.on("unselect", (e) => onUnselect(e)); - cy.on("add", "node", (e) => onNodeAdded(e)); - cy.on("add", "edge", (e) => onEdgeAdded(e)); - cy.on("remove", "node", (e) => onNodeRemoved(e)); - cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); - cy.on("mouseover", "edge", (e) => onEdgeHover(e)); - cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); - cy.on("mouseover", "node", (e) => onNodeHover(e)); - cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); - // Handles dragging - cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); - cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); - cy.on("cxttap", "node", (e) => onCtxTap(e)); - + cy.on("cxttap", "node", (e) => onCtxTap(e)); - if (selectedAction.id !== undefined && selectedAction.id !== null && selectedAction.id !== "") { - setTimeout(() => { - const foundaction = cy.$id(selectedAction.id) - if (foundaction !== undefined && foundaction !== null) { - foundaction.select() - } - }, 250) - } - } + + if (selectedAction.id !== undefined && selectedAction.id !== null && selectedAction.id !== "") { + setTimeout(() => { + const foundaction = cy.$id(selectedAction.id) + if (foundaction !== undefined && foundaction !== null) { + foundaction.select() + } + }, 250) + } + } - // Need to run through graph setup with this one - }}> -
    - - - {translatedDate} - {/* {newrevision.edited.toString().slice(6,10)} | {newrevision.revision_id.slice(0,5)} */} - - - - - - - -
    - {/*revision.edited === originalWorkflow.edited ? + // Need to run through graph setup with this one + }}> +
    + + + {translatedDate} + {/* {newrevision.edited.toString().slice(6,10)} | {newrevision.revision_id.slice(0,5)} */} + + + + + + + +
    + {/*revision.edited === originalWorkflow.edited ? Current version : null*/} -
    - {revision.actions !== undefined && revision.actions !== null ? - - - - - {revision.actions.length} - - - - : null} - {revision.triggers !== undefined && revision.triggers !== null ? - - - - - {revision.triggers.length} - - - - : null} -
    - {revision.updated_by !== undefined && revision.updated_by !== null && revision.updated_by !== "" ? - - {revision.updated_by} - - : null} -
    - ) - } +
    + {revision.actions !== undefined && revision.actions !== null ? + + + + + {revision.actions.length} + + + + : null} + {revision.triggers !== undefined && revision.triggers !== null ? + + + + + {revision.triggers.length} + + + + : null} +
    + {revision.updated_by !== undefined && revision.updated_by !== null && revision.updated_by !== "" ? + + {revision.updated_by} + + : null} +
    + ) + } - const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ? -
    - - Version History - - - Versions are stored for every change made, up to once per minute. When restoring a version, the changes will not take effect until you save the workflow. - - -
    - {/* + const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ? +
    + + Version History + + + Versions are stored for every change made, up to once per minute. When restoring a version, the changes will not take effect until you save the workflow. + + +
    + {/*
    @@ -22268,85 +23964,85 @@ const releaseToConnectLabel = "Release to Connect" */} - {allRevisions.length > 0 ? -
    - { - allRevisions.map((revision, index) => { - /* - if(revision.edited === selectedVersion.edited){ - return null - } - */ + {allRevisions.length > 0 ? +
    + { + allRevisions.map((revision, index) => { + /* + if(revision.edited === selectedVersion.edited){ + return null + } + */ - return ( - - ) - }) - } -
    + showBorder={revision.edited === selectedVersion.edited} + /> + ) + }) + } +
    - : -
    - - No other revisions found. Save your workflow with changes to create a revision. - -
    - } -
    + : +
    + + No other revisions found. Save your workflow with changes to create a revision. + +
    + } +
    - : null + : null - const workflowRevisions = !showWorkflowRevisions ? null : -
    - { - //setShowWorkflowRevisions(false) - }} - style={{ - resize: "both", - overflow: "hidden", - zIndex: 10005, - }} - hideBackdrop={true} - variant="persistent" - BackdropProps={{ - style: { - //backgroundColor: "transparent", - } - }} - PaperProps={{ - style: { - resize: "both", - overflow: "hidden", - minWidth: isMobile ? "100%" : 360, - maxWidth: isMobile ? "100%" : 360, - backgroundColor: theme.palette.platformColor, - color: "white", - fontSize: 18, - zIndex: 15001, - borderRight: theme.palette.defaultBorder, + const workflowRevisions = !showWorkflowRevisions ? null : +
    + { + //setShowWorkflowRevisions(false) + }} + style={{ + resize: "both", + overflow: "hidden", + zIndex: 10005, + }} + hideBackdrop={true} + variant="persistent" + BackdropProps={{ + style: { + //backgroundColor: "transparent", + } + }} + PaperProps={{ + style: { + resize: "both", + overflow: "hidden", + minWidth: isMobile ? "100%" : 360, + maxWidth: isMobile ? "100%" : 360, + backgroundColor: theme.palette.platformColor, + color: "white", + fontSize: 18, + zIndex: 15001, + borderRight: theme.palette.defaultBorder, - paddingLeft: leftSideBarOpenByClick ? 280 : 100, - transition: "padding-left 0.3s", + paddingLeft: leftSideBarOpenByClick ? 280 : 100, + transition: "padding-left 0.3s", - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", - }, - }} - > - {drawerData} - -
    -
    - {/*selectedRevision.edited !== undefined && selectedRevision.edited !== null && selectedRevision.edited !== originalWorkflow.edited ? + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", + }, + }} + > + {drawerData} + +
    +
    + {/*selectedRevision.edited !== undefined && selectedRevision.edited !== null && selectedRevision.edited !== originalWorkflow.edited ?
    -
    - - {selectedVersion?.name} - -
    -
    - -
    -
    -
    +
    +
    + + {selectedVersion?.name} + +
    +
    + +
    +
    +
    - const changeActionParameterCodeMirror = (event, count, data, actionlist) => { - // Check if event.target.value is an array. If it is, split with comma + const changeActionParameterCodeMirror = (event, count, data, actionlist, parametername, selectedAction, setSelectedAction) => { - if (data.startsWith("${") && data.endsWith("}")) { - // PARAM FIX - Gonna use the ID field, even though it's a hack - const paramcheck = selectedAction.parameters.find(param => param.name === "body") - if (paramcheck !== undefined) { - // Escapes all double quotes - const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); - if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { - paramcheck["value_replace"] = [{ - "key": data.name, - "value": toReplace, - }] + // FIXME: This exists ONLY to make sure focus + blur actually changes the field value + // in fields from ParsedAction.jsx such as rightside_field_2 + const simulateTyping = (inputElement, text) => { + if (!inputElement) { + console.error("Target element not found!") + return; + } - } else { - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - "key": data.name, - "value": toReplace, - }) - } else { - paramcheck["value_replace"][subparamindex]["value"] = toReplace - } - } + //console.log("Simulating typing for: ", text, "in", inputElement) - if (paramcheck["value_replace"] === undefined) { - selectedAction.parameters[count]["value_replace"] = paramcheck - } else { - //selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] - selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] - } - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - return - } - } + inputElement.value = "" + setTimeout(() => { + text.split("").forEach((char, index) => { + setTimeout(() => { + inputElement.value = text.slice(0, index + 1) - if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) { - var curstring = "" - var record = false - for (let [key,keyval] in Object.entries(selectedAction.parameters[count].value)) { - const item = selectedAction.parameters[count].value[key] - if (record) { - curstring += item - } + // Simulate an event object like React's synthetic event + const event = { + target: { + value: inputElement.value, + }, + }; - if (item === "$") { - record = true - curstring = "" - } - } + // Find the onChange handler, assuming you're calling it from here + if (typeof inputElement.onchange === 'function') { + inputElement.onchange(event); // Call onChange with the synthetic event + } - if (curstring.length > 0 && actionlist !== null) { - // Search back in the action list - curstring = curstring.split(" ").join("_").toLowerCase() - var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) - if (actionItem !== undefined) { - console.log("Found item: ", actionItem) + // Dispatch the input event for React's internal event system + const inputEvent = new Event("input", { bubbles: true }); + inputElement.dispatchEvent(inputEvent); - var jsonvalid = true - try { - const tmp = String(JSON.parse(actionItem.example)) - if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - } - } - } + }, 10) + }) + }, 50) - if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { - const parsedvalue = data - if (parsedvalue.includes("#")) { - const splitparsed = parsedvalue.split(".#.") - //console.log("Cant contain #: ", splitparsed) - if (splitparsed.length > 1) { - //data.value = splitparsed[0] + setTimeout(() => { + inputElement.focus() + }, 500) + }; - selectedAction.parameters[0].value = splitparsed[0] - selectedAction.parameters[1].value = splitparsed[1] + // Check if event.target.value is an array. If it is, split with comma + if (parametername !== undefined && parametername.startsWith("${") && parametername.endsWith("}")) { + var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername) + if (paramcheckIndex !== -1) { + // Replace the value in the field + const toReplace = data.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); + selectedAction.parameters[paramcheckIndex].value = toReplace + setSelectedAction(selectedAction) + setUpdate(Math.random()) - selectedAction.parameters[0].autocompleted = true - selectedAction.parameters[1].autocompleted = true - setUpdate(Math.random()) - } - } - } else { - if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) { - selectedAction.parameters[count].autocompleted = false - selectedAction.parameters[count].value = data - } - } + // Find the fieldname + const clickedFieldId = "rightside_field_" + count; + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== undefined && clickedField !== null) { + simulateTyping(clickedField, toReplace) - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - } + /* + clickedField.value = toReplace + const newEvent = new Event("input", { bubbles: true }) + Object.defineProperty(event, "target", { + value: { ...clickedField, value: toReplace }, + writable: false, + }) + + //const newEvent = new Event("input", { bubbles: true }) + clickedField.dispatchEvent(newEvent) + console.log("Found field: ", clickedField) + */ + } + + //toast("Replaced field!") + + return + } + } + + if (data.startsWith("${") && data.endsWith("}")) { + console.log("Changing field with variable: ", data) + + // PARAM FIX - Gonna use the ID field, even though it's a hack + var paramcheck = selectedAction.parameters.find(param => param.name === "body") + if (paramcheck !== undefined) { + // Escapes all double quotes + const toReplace = event.target.value.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [{ + "key": data.name, + "value": toReplace, + }] + + } else { + const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + "key": data.name, + "value": toReplace, + }) + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace + } + } + + if (paramcheck["value_replace"] === undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck + } else { + //selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] + } + + setSelectedAction(selectedAction) + return + } + } + + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + var curstring = "" + var record = false + for (let [key, keyval] in Object.entries(selectedAction.parameters[count].value)) { + const item = selectedAction.parameters[count].value[key] + if (record) { + curstring += item + } + + if (item === "$") { + record = true + curstring = "" + } + } + + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase() + var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) + if (actionItem !== undefined) { + console.log("Found item: ", actionItem) + + var jsonvalid = true + try { + const tmp = String(JSON.parse(actionItem.example)) + if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + } + } + } + + if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { + const parsedvalue = data + if (parsedvalue.includes("#")) { + const splitparsed = parsedvalue.split(".#.") + //console.log("Cant contain #: ", splitparsed) + if (splitparsed.length > 1) { + //data.value = splitparsed[0] + + selectedAction.parameters[0].value = splitparsed[0] + selectedAction.parameters[1].value = splitparsed[1] + + selectedAction.parameters[0].autocompleted = true + selectedAction.parameters[1].autocompleted = true + setUpdate(Math.random()) + } + } + } else { + if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) { + selectedAction.parameters[count].autocompleted = false + selectedAction.parameters[count].value = data + } + } + + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + } + + const handleActionParamChange = (actionId, fieldName, newData) => { + if (workflow !== undefined) { + // Find the action with matching id + const actionIndex = workflow?.actions.findIndex(action => action.id === actionId); + if (actionIndex >= 0) { + // Find the parameter with matching name + console.log("fieldName", fieldName) + const paramIndex = workflow.actions[actionIndex].parameters.findIndex(param => param.name === fieldName); + if (paramIndex >= 0) { + // Update the parameter value + workflow.actions[actionIndex].parameters[paramIndex].value = newData; + + // Update workflow state to trigger re-render + setWorkflow({...workflow}); + setLastSaved(false); + } + } + } + } /* var foundusecase = {} if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0 && userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) { - for (let priokey in userdata.priorities) { - const prio = userdata.priorities[priokey] - if (prio.type !== "usecase") { - continue - } + for (let priokey in userdata.priorities) { + const prio = userdata.priorities[priokey] + if (prio.type !== "usecase") { + continue + } - const descsplit = prio.description.split("&") - var srcapp = "" - var dstapp = "" - if (descsplit.length > 0) { - srcapp = descsplit[0].toLowerCase().replaceAll(" ", "_") + const descsplit = prio.description.split("&") + var srcapp = "" + var dstapp = "" + if (descsplit.length > 0) { + srcapp = descsplit[0].toLowerCase().replaceAll(" ", "_") - if (descsplit.length > 2) { - dstapp = descsplit[2].toLowerCase().replaceAll(" ", "_") - } - } + if (descsplit.length > 2) { + dstapp = descsplit[2].toLowerCase().replaceAll(" ", "_") + } + } - if (srcapp.length > 0 && dstapp.length > 0) { - for (let actionkey in workflow.actions) { - const curaction = workflow.actions[actionkey] - const appname = curaction.app_name.toLowerCase().replaceAll(" ", "_") - if (appname === srcapp || appname === dstapp) { - foundusecase = prio - break - } - } - } + if (srcapp.length > 0 && dstapp.length > 0) { + for (let actionkey in workflow.actions) { + const curaction = workflow.actions[actionkey] + const appname = curaction.app_name.toLowerCase().replaceAll(" ", "_") + if (appname === srcapp || appname === dstapp) { + foundusecase = prio + break + } + } + } - if (foundusecase.name !== undefined && foundusecase.name !== null && foundusecase.name !== "") { - break - } - } + if (foundusecase.name !== undefined && foundusecase.name !== null && foundusecase.name !== "") { + break + } + } } const templatePopup = foundusecase.name === undefined || foundusecase.name === null || foundusecase.name === "" ? null : - -
    - +
    + -
    - - */ + srcapp={foundusecase.description.split("&")[0]} + img1={foundusecase.description.split("&")[1]} + dstapp={foundusecase.description.split("&")[2]} + img2={foundusecase.description.split("&")[3]} + /> +
    +
    + */ const loadedCheck = isLoaded && workflowDone ? ( -
    +
    {newView} - {aiQueryModal} + {aiQueryModal} {conditionsModal} {codePopoutModal} - {workflowRevisions} + {workflowRevisions} {authenticationModal} {tenzirConfigModal} - {/*editWorkflowModal*/} - {authgroupModal} - {executionArgumentModal} + {authgroupModal} + {executionArgumentModal} {configureWorkflowModal} - {/*usecaseSlidein*/} - + - {codeEditorModalOpen ? - - : null} + setAiQueryModalOpen={setAiQueryModalOpen} + /> + : null} {editWorkflowModalOpen === true ? : null} - + {/*selectionOpen === true ?
    @@ -22628,12 +24431,12 @@ const releaseToConnectLabel = "Release to Connect"
    : null*/} -
    +
    {showVideo !== undefined && showVideo.length > 0 ?
    @@ -22674,12 +24477,12 @@ const releaseToConnectLabel = "Release to Connect" />
    ) : ( -
    - - - Loading Workflow & Apps... - -
    +
    + + + Loading Workflow & Apps... + +
    ); // Awful way of handling scroll diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index 8de63fb9..96298437 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -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 + {appAuthentication?.length > 0 ? appAuthentication.map((appAuth) => (
    { backgroundColor: '#1f1f1f', color: 'white', padding: '5px', + textAlign: "left", }} > { @@ -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 ? + + {appAuth?.app?.name} + + : null} {appAuth?.validation?.valid === true ? @@ -1631,7 +1686,9 @@ const ApiExplorerWrapper = (props) => { >
    - {isLoggedIn === true ? + {openapi?.id === "HTTP" ? + null + : isLoggedIn === true ?

    Tags

    { 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, }} > { 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
    { { {testView} */} -
    +
    {appDownloadData.length > 0 ? diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx new file mode 100644 index 00000000..b3cb0d99 --- /dev/null +++ b/frontend/src/views/AppExplorer.jsx @@ -0,0 +1,4644 @@ +import React, { useState, useEffect, useContext } from "react"; + +import theme from "../theme.jsx"; +import ReactGA from "react-ga4"; +import Markdown from "react-markdown"; +import algoliasearch from "algoliasearch/lite"; +import ReactJson from "react-json-view-ssr"; +//import { useAlert +import { ToastContainer, toast } from "react-toastify" +import { makeStyles, createStyles } from "@mui/styles"; +import { useParams, useNavigate, Link } from "react-router-dom"; + +import { + Autocomplete, + Box, + Zoom, + Card, + CardActionArea, + Fade, + Tabs, + Tab, + CircularProgress, + DialogActions, + Dialog, + DialogTitle, + DialogContent, + Tooltip, + IconButton, + Menu, + Paper, + Button, + Typography, + Divider, + MenuItem, + Avatar, + TextField, + Breadcrumbs, + Checkbox, + Chip, + Select, +} from "@mui/material"; + +import { + Business as BusinessIcon, + Edit as EditIcon, + CloudDownload as CloudDownloadIcon, + Warning as WarningIcon, + VerifiedUser as VerifiedUserIcon, + Close as CloseIcon, + LockOpen as LockOpenIcon, + PlayArrow as PlayArrowIcon, + GetApp as GetAppIcon, + Apps as AppsIcon, + Description as DescriptionIcon, + ShowChart as ShowChartIcon, + Person as PersonIcon, + Polyline as PolylineIcon, + OpenInNew as OpenInNewIcon, +} from "@mui/icons-material"; + +import ForkRightIcon from '@mui/icons-material/ForkRight'; + +import Alert from "@mui/material/Alert"; +import { Context } from "../context/ContextApi.jsx"; + +import { + SearchBox, + StaticRefinementList, + RefinementList, + InstantSearch, + connectSearchBox, + connectHits, + Index, +} from "react-instantsearch-dom"; +import AppStats from "../components/AppStats.jsx"; +import ParsedAction from "../components/ParsedAction.jsx"; +import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; +import { base64_decode, appCategories } from "../views/AppCreator.jsx"; +import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import AuthenticationWindow from "../components/AuthenticationWindow.jsx"; +import { CodeHandler, Img, OuterLink, CopyToClipboard, } from "../views/Docs.jsx"; +import { useStyles, } from "../components/ParsedAction.jsx"; +import { sortByKey } from "../views/AngularWorkflow.jsx"; + +import { v4 as uuidv4 } from "uuid"; +import aa from "search-insights"; + +const surfaceColor = "#27292D"; +const inputColor = "#383B40"; + +const chipStyle = { + marginTop: 5, + backgroundColor: "#3d3f43", + height: 30, + marginRight: 5, + paddingLeft: 5, + paddingRight: 5, + height: 28, + cursor: "pointer", + borderColor: "#3d3f43", + color: "white", +}; + +const actionListStyle = { + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + marginTop: 5, + backgroundColor: inputColor, + display: "flex", + color: "white", + maxWidth: 350, + minWidth: 350, + maxHeight: 54, + overflow: "hidden", +}; + +const boxStyle = { + color: "white", + flex: "3", + margin: 10, + paddingLeft: 30, + paddingRight: 30, + paddingBottom: 30, + paddingTop: 30, + display: "flex", + flexDirection: "column", + position: "relative", + maxHeight: 180, + overflow: "hidden", +}; + +const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; + + +// AppTypes: +// 0 = OpenAPI (VALID) +// 1 = Normal app (Python) +// 2 = OpenAPI (Invalid) +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +) + +const AppExplorer = (props) => { + const { + globalUrl, + userdata, + setUserData, + checkLogin, + isLoaded, + selectedApp, + serverside, + isMobile, + isLoggedIn, + selectedDoc, + secondApp, + } = props; + + //const alert = useAlert(); + const classes = useStyles() + let navigate = useNavigate() + + const { leftSideBarOpenByClick, } = useContext(Context); + + const params = useParams(); + //var props = JSON.parse(JSON.stringify(defaultprops)) + //props.match = {} + //params = params + + const bodyDivStyle = { + margin: "auto", + maxWidth: isMobile ? "100%" : 1350, + scrollX: "hidden", + overflowX: "hidden", + }; + + var upload = ""; + const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"]; + const authenticationOptions = [ + "No authentication", + "API key", + "Bearer auth", + "Basic auth", + ]; + const apikeySelection = ["Header", "Query"]; + + const [app, setApp] = useState({}); + + const [openapi, setOpenapi] = useState({}); + const [name, setName] = useState(""); + const [appId, setAppId] = useState(""); + const [contact, setContact] = useState(""); + const [file] = useState(""); + const [fileBase64, setFileBase64] = useState(""); + const [isAppLoaded, setIsAppLoaded] = useState(false); + const [, setDescription] = useState(""); + const [, setBaseUrl] = useState(""); + const [, setAuthenticationRequired] = useState(false); + const [, setAuthenticationOption] = useState(authenticationOptions[0]); + const [newWorkflowTags, setNewWorkflowTags] = React.useState([]); + const [, setParameterName] = useState(""); + const [, setParameterLocation] = useState( + apikeySelection.length > 0 ? apikeySelection[0] : "" + ); + const [, setUrlPath] = useState(""); + const [urlPathQueries, setUrlPathQueries] = useState([]); + const [, setBasedata] = React.useState({}); + const [actions, setActions] = useState([]); + const [errorCode] = useState(""); + const [reloadUrl, setReloadUrl] = React.useState( + serverside === true ? "" : window.location.href + ); + const [relatedWorkflows, setRelatedWorkflows] = useState(0); + const [relatedApps, setRelatedApps] = useState(0); + const [appAuthentication, setAppAuthentication] = React.useState([]); + const [authLoaded, setAuthLoaded] = useState(false); + const baseResult = "The execution result will show up here"; + const [anchorElAvatar, setAnchorElAvatar] = React.useState(null); + const [executionResult, setExecutionResult] = useState({ + valid: false, + result: baseResult, + }); + const [executing, setExecuting] = useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const [creatorProfile, setCreatorProfile] = React.useState({}); + const [selectedTab, setSelectedTab] = React.useState(0); + const defaultDocs = `\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app.` + const [sharingConfiguration, setSharingConfiguration] = React.useState("you"); + const [appdata, setAppData] = React.useState({}); + const [appDocumentation, setAppDocumentation] = useState(defaultDocs) + const [secondaryApp, setSecondaryApp] = useState({}); + const [firstRequest, setFirstRequest] = useState(true); + const [publishModalOpen, setPublishModalOpen] = React.useState(false); + + const [categories, setCategories] = useState(appCategories) + const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]); + const [update, setUpdate] = useState(""); + const [triggers, setTriggers] = useState([]) + const [selectedOrganization, setSelectedOrganization] = React.useState(undefined) + const [selectedValidationAction, setSelectedValidationAction] = React.useState({}) + + const [selectedMeta, setSelectedMeta] = React.useState({ + link: "https://github.com/Shuffle/openapi-apps/new/master/docs", + read_time: 1, + }) + + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + + + // FIXME: This is used, as useEffect() creates an issue with apps not loading at all + var to_be_copied = ""; + + // 0 = VALID OpenAPI, 1 = Python, 2 = INVALID OpenAPI + const [appType, setAppType] = React.useState(0); + + function handleClick(event) { + setAnchorEl(event.currentTarget); + } + + function handleClose() { + setAnchorEl(null); + } + + + const loadOrganization = (orgId) => { + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + + } else { + setSelectedOrganization(responseJson) + } + }) + .catch((error) => { + console.log("Error in fetching organization: ", error) + }) + } + + + useEffect(() => { + if (selectedApp !== undefined && selectedApp !== null && Object.getOwnPropertyNames(selectedApp).length > 0) { + //console.log("Firstrequest!!!") + } else { + if (serverside) { + console.log("Not getting app because serverside."); + } else { + if (params.appid.length === 32 || params.appid.length === 36) { + handleEditApp(params.appid); + runAlgoliaAppSearch(params.appid, false, true); + } else { + runAlgoliaAppSearch(params.appid); + + //handleEditApp() + } + } + //parseIncomingOpenapiData(YAML.parse(data)) + } + + if (serverside !== true) { + const urlSearchParams = new URLSearchParams(window.location.search); + const queries = Object.fromEntries(urlSearchParams.entries()); + const foundTab = queries["tab"]; + console.log("PROPS: ", queries); + + if (params.integrationid !== undefined) { + console.log( + "Should search for connection integration with ", + params.integrationid + ); + setSelectedTab(3); + + runAlgoliaAppSearch(params.integrationid, false); + } else if (foundTab !== null && foundTab !== undefined) { + if (foundTab === "stats") { + setSelectedTab(2); + } else if (foundTab === "run") { + setSelectedTab(1); + } else if (foundTab === "docs" || foundTab === "documentation") { + setSelectedTab(0); + } + } else { + //setSelectedTab(1); + } + + } + }, []); + + if (serverside === false && firstRequest && isLoggedIn === true && selectedOrganization === undefined && userdata !== undefined && userdata.active_org !== undefined && userdata.active_org !== null && userdata.active_org.id !== undefined && userdata.active_org.id !== null) { + loadOrganization(userdata.active_org.id) + } + + var activateButton = ( + + + + ); + + const Heading = (props) => { + const element = React.createElement(`h${props.level}`,{ style: { marginTop: props.level === 1 ? 20 : 50 } },props.children); + + const [hover, setHover] = useState(false); + + var extraInfo = ""; + if (props.level === 1) { + extraInfo = ( +
    +
    + {isMobile === true ? null : ( + + { + ReactGA.event({ + category: "Appexplorer", + action: "github_docs_edit_click", + label: params.appid, + }); + }} + > + + + + )} + {isMobile === true ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    + +
    + {isMobile === true || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + ); + } + + // Still inside Heading + + // Find appCategory in appCategories + const appCategory = newWorkflowCategories.length === 0 ? "" : (newWorkflowCategories[0]).toLowerCase(); + const foundCategory = appCategories.find((category) => { + if (category.name.toLowerCase() === appCategory) { + return category + } + }) + const bgColor = foundCategory === undefined ? "" : foundCategory.color + + const getAllLabels = () => { + const actionLabels = actions.filter((input_action) => { + return input_action.action_label !== undefined && input_action.action_label !== null && input_action.action_label !== "No Label" + }) + + var labels = [] + actionLabels.forEach((action) => { + labels.push(action.action_label) + }) + + return labels + } + + const allLabels = getAllLabels() + + const findRelevantAction = (action_label) => { + const foundAction = actions.find((input_action) => { + if (input_action.action_label.toLowerCase() === action_label.toLowerCase()) { + return input_action + } + }) + + if (foundAction !== undefined) { + setCurrentAction(foundAction) + setCurrentActionMethod(foundAction.method); + setSelectedTab(1); + } else { + console.log("Could not find action with label: ", action_label) + } + } + + // Parses out extra category info and such for the app + const extraAppInfo = props.level === 1 ? +
    + {/* +
    + + Category: + + +
    + */} + {triggers.length === 0 ? null : +
    + {triggers.map((trigger, index) => { + + return ( + { + console.log("Clicked: ", trigger.name) + }} + style={{ + cursor: "pointer", + color: "white", + borderRadius: 40, + minWidth: 80, + marginRight: 10, + marginTop: 2, + fontSize: 14, + }} + avatar={} + label={trigger.name} + /> + ) + })} +
    + } + + {serverside === false && foundCategory !== undefined && foundCategory !== null && foundCategory.action_labels.length > 0 ? +
    + {foundCategory.action_labels.slice(0,5).map((action_label, index) => { + const included = allLabels.includes(action_label) + const iconInfo = GetIconInfo({ name: action_label }); + const useIcon = iconInfo.originalIcon; + + return ( + { + findRelevantAction(action_label) + }} + disabled={included === false} + style={{ + cursor: included ? "pointer" : "default", + color: "white", + borderRadius: 40, + minWidth: 80, + marginRight: 10, + marginTop: 2, + fontSize: 14, + textDecoration: included ? "none" : "line-through", + }} + avatar={useIcon} + label={action_label} + /> + ) + })} +
    + : null} +
    + : null + + return ( + { + setHover(true); + }} + > + {props.level !== 1 ? ( + + ) : null} + {element} + {extraAppInfo} + {extraInfo} + + ); + }; + + const [, setCurrentActionMethod] = useState(actionNonBodyRequest[0]); + + // Selectedaction = Shuffle style action + // Currentaction = OpenAPI style + const [selectedAction, setSelectedAction] = useState({}); + const [currentAction, setCurrentAction] = useState({ + name: "", + description: "", + url: "", + headers: "", + paths: [], + queries: [], + body: "", + errors: [], + method: actionNonBodyRequest[0], + }); + + if (params.appid === "new") { + return null; + } + + const WorkflowHits = ({ hits }) => { + //console.log("WORKFLOWS: ", hits) + + setRelatedWorkflows(hits.length); + return hits.length; + }; + + const AppHits = ({ hits }) => { + if (hits.length >= 1) { + setRelatedApps(hits.length - 1); + + return hits.length - 1; + } else { + setRelatedApps(0); + return 0; + } + }; + + const getUserProfile = (username) => { + if (serverside === true) { + return; + } + + fetch(`${globalUrl}/api/v1/users/creators/${username}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setCreatorProfile(responseJson); + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => { + return null; + }; + + const CustomWorkflowHits = connectHits(WorkflowHits); + const CustomAppHits = connectHits(AppHits); + const CustomSearchBox = connectSearchBox(SearchBox); + + const HandleJsonCopy = (base, copy, base_node_name) => { + console.log("COPY: ", copy); + var newitem = JSON.parse(base); + to_be_copied = "$" + base_node_name; + for (var key in copy.namespace) { + if (copy.namespace[key].includes("Results for")) { + continue; + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.namespace[key]]; + if (!isNaN(copy.namespace[key])) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.namespace[key]; + } + } + } + }; + + const handleReactJsonClipboard = (copy) => { + console.log("COPY: ", copy); + + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + navigator.clipboard.writeText(JSON.stringify(copy)); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied data"); + } + }; + + const activateApp = (action) => { + if (serverside === true) { + return + } + + const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) + var url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate` + if (action !== undefined && action !== null) { + url = `${globalUrl}/api/v1/apps/${appId}/${action}` + } + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Failed to activate"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (action === undefined || action === null) { + if (responseJson.reason !== undefined) { + toast("Failed to activate the app: "+responseJson.reason); + } else { + toast("Failed to activate the app"); + } + } else { + if (responseJson.reason !== undefined) { + toast("Failed to perform action: "+responseJson.reason); + } else { + toast("Failed to perform action. Please try again or contact support@shuffler.io"); + } + } + } else { + if (checkLogin !== undefined && checkLogin !== null) { + checkLogin() + } + + if (action === undefined || action === null) { + if (appExists) { + toast("App deactivated for your organization! Existing workflows with the app will continue to work.") + } else { + toast("App activated for your organization!") + } + } else { + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleEditApp = (appid) => { + if (serverside === true) { + return; + } + + setAppId(appid) + + fetch(globalUrl + "/api/v1/apps/" + appid + "/config", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + //console.log("App doesn't exist or isn't available to you.") + //toast("Something went wrong - this app is not available to you. Redirecting you back to search.") + ReactGA.event({ + category: "appexplorer", + action: `app_not_found`, + label: appid, + }); + } else { + ReactGA.event({ + category: "appexplorer", + action: `app_found`, + label: appid, + }); + } + + return response.json(); + }) + .then((responseJson) => { + if ( + responseJson.success === false || + responseJson.success === undefined + ) { + toast("Failed to get the app") + setIsAppLoaded(true) + setTimeout(() => { + navigate("/search") + }, 1000); + return; + } else { + parseIncomingOpenapiData(responseJson); + } + }) + .catch((error) => { + toast("Error in app fetch: " + error.toString()); + }); + }; + + const parseIncomingAppdata = (data, openapiExists) => { + document.title = data.name + " App - OpenAPI and API"; + + + + setExecutionResult({ + valid: false, + result: baseResult, + }); + + setName((data.name.charAt(0).toUpperCase() + data.name.substring(1)).replaceAll("_"," ")); + + setDescription(data.description); + setFileBase64(data.large_image); + setContact(data.contact_info); + + if (data.categories !== undefined && data.categories !== null) { + setNewWorkflowCategories(data.categories); + } + setAppType(1); + + if (data.owner !== undefined && data.owner !== null) { + getUserProfile(data.owner); + //console.log("DATA: ", data) + } + + setAppData(data) + if (data.reference_info.triggers !== undefined && data.reference_info.triggers !== null && data.reference_info.triggers.length > 0) { + var parsedtriggers = [] + for (var key in data.reference_info.triggers) { + const curtrigger = data.reference_info.triggers[key] + + const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger.toLowerCase()) + if (foundTrigger !== undefined && foundTrigger !== null) { + parsedtriggers.push(foundTrigger) + } + } + + setTriggers(parsedtriggers) + } + + var newactions = []; + if (!openapiExists) { + console.log("Skipping openapi"); + for (var key in data.actions) { + const action = data.actions[key]; + newactions.push({ + name: action.name, + description: action.description, + url: "", + headers: "", + paths: [], + queries: [], + body: "", + errors: [], + method: "CUSTOM", + }); + } + } + + + if (newactions.length > 0) { + setCurrentAction(newactions[0]); + + if (data.actions !== undefined) { + //var methodName = `${data.method}_${data.name}`.toLowerCase() + //if (data.name.toLowerCase().startsWith(data.method.toLowerCase())) { + // methodName = data.name.toLowerCase() + //} + //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === methodName) + //if (newselectedaction === undefined || newselectedaction === null) { + // toast(`Name ${methodName} not found. Please contact us.`) + // return + //} + + //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === ) + const newselectedaction = data.actions[0]; + newselectedaction.app_id = data.id; + newselectedaction.app_name = data.name; + newselectedaction.app_version = data.app_version; + + newselectedaction.authentication = selectedAction.authentication; + + newselectedaction.authentication_id = selectedAction.authentication_id; + newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication; + + if ( + data.authentication.required && + newselectedaction.authentication_id !== undefined && + newselectedaction.authentication_id !== null && + newselectedaction.authentication_id.length === 0 + ) { + const tmpParams = selectedAction.parameters; + selectedAction.parameters = []; + + for (let paramkey in data.authentication.parameters) { + var item = data.authentication.parameters[paramkey]; + console.log("PARAM1: ", item) + item.configuration = true; + + const found = selectedAction.parameters.find((param) => param.name === item.name); + + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + + for (let paramkey in tmpParams) { + var item = tmpParams[paramkey]; + console.log("PARAM2: ", item) + //item.configuration = true + const found = selectedAction.parameters.find((param) => param.name === item.name); + + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + } + + setSelectedAction(newselectedaction); + } + + + const firstActions = newactions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") + //console.log("First actions: ", firstActions) + const secondActions = newactions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") + const newActions = firstActions.concat(secondActions) + setActions(newActions); + } + + setIsAppLoaded(true); + }; + + // Sets the data up as it should be at later points + // This is the data FROM the database, not what's being saved + const parseIncomingOpenapiData = (data) => { + var appexists = false; + var nameExists = false; + var parsedapp = {}; + if (data.app !== undefined && data.app !== null) { + // Should basically always be true if openapi exists too + var parsedBaseapp = "" + try { + parsedBaseapp = base64_decode(data.app) + } catch (e) { + console.log("Failed JSON parsing: ", e) + parsedBaseapp = data + } + + parsedapp = JSON.parse(parsedBaseapp) + parsedapp.name = parsedapp.name.replaceAll("_", " "); + + setAppDocumentation("# "+parsedapp.name+defaultDocs); + setApp(parsedapp); + setSharingConfiguration(parsedapp.sharing === true ? "public" : "you") + + appexists = + parsedapp.name !== undefined && + parsedapp.name !== null && + parsedapp.name.length !== 0; + + if (appexists) { + getAppDocs(parsedapp.name, "python", parsedapp.app_version); + } + + if (data.openapi === undefined || data.openapi === null) { + console.log("Parsed app: ", parsedapp) + parseIncomingAppdata(parsedapp, false); + } else { + parseIncomingAppdata(parsedapp, true); + } + } + + if (data.openapi === undefined || data.openapi === null) { + return; + } + + var parsedDecoded = "" + try { + parsedDecoded = base64_decode(data.openapi) + } catch (e) { + console.log("Failed JSON parsing: ", e) + parsedDecoded = data + } + + setAppType(0); + parsedapp = JSON.parse(parsedDecoded); + data = parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body); + setOpenapi(data); + + getAppDocs(data.info.title, "openapi", data.app_version); + + setBasedata(data); + if (!appexists) { + setName( + ( + data.info.title.charAt(0).toUpperCase() + data.info.title.substring(1) + ).replaceAll("_", " ") + ); + setDescription(data.info.description); + + console.log("Found name: ", data.info.title); + } + + if (serverside !== true) { + var doctitle = "Shuffle App for " + data.info.title + if (!data.info.title.toLowerCase().includes("api")) { + doctitle += " API" + } + + document.title = doctitle + } + + if (data.info !== null && data.info !== undefined) { + if (data.info["x-logo"] !== undefined) { + setFileBase64(data.info["x-logo"]); + } + + if (data.info.contact !== undefined) { + setContact(data.info.contact); + } + + if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) { + setNewWorkflowCategories(data.info["x-categories"]); + } + } + + if (data.tags !== undefined && data.tags.length > 0) { + for (var key in data.tags) { + newWorkflowTags.push(data.tags[key].name); + } + + setNewWorkflowTags(newWorkflowTags); + } + + // This is annoying (: + var securitySchemes = data.components.securityDefinitions; + if (securitySchemes === undefined) { + securitySchemes = data.securitySchemes; + } + + if (securitySchemes === undefined) { + securitySchemes = data.components.securitySchemes; + } + + const allowedfunctions = [ + "GET", + "CONNECT", + "HEAD", + "DELETE", + "POST", + "PATCH", + "PUT", + ]; + + // FIXME - headers? + var newActions = []; + var wordlist = {}; + if (data.paths !== null && data.paths !== undefined) { + for (let [path, pathvalue] of Object.entries(data.paths)) { + if (path === "tmp0") { + setAppType(2); + } + + for (let [method, methodvalue] of Object.entries(pathvalue)) { + if (methodvalue === null) { + toast("Skipped method " + method); + continue; + } + + if (!allowedfunctions.includes(method.toUpperCase())) { + continue; + } + + var tmpname = methodvalue.summary; + if ( + methodvalue.operationId !== undefined && + methodvalue.operationId !== null && + methodvalue.operationId.length > 0 + ) { + tmpname = methodvalue.operationId; + } + + var newaction = { + name: tmpname, + description: methodvalue.description, + url: path, + method: method.toUpperCase(), + headers: "", + queries: [], + paths: [], + body: "", + errors: [], + example_response: "", + action_label: "No Label", + required_bodyfields: [], + } + + // Related to Label Management + if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) { + // Check if there are commas in it then loop and find the correct one + // Should ignore 'No Label' and 'No label' + var correctlabel = "" + const labels = methodvalue["x-label"].split(",") + for (let labelkey in labels) { + var label = labels[labelkey].trim() + if (label.toLowerCase() === "no label") { + continue + } + + // Remove quotes and escapes + label = label.replace(/['"]+/g, '') + label = label.replace(/\\/g, '') + + //label = label.replace("_", " ", -1) + //label = label.charAt(0).toUpperCase() + label.slice(1) + + correctlabel = label + break + } + + // FIX: Map labels only if they're actually in the category list + newaction.action_label = correctlabel + } + + if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) { + newaction.required_bodyfields = methodvalue["x-required-fields"] + } + + for (key in methodvalue.parameters) { + const parameter = methodvalue.parameters[key]; + if (parameter.in === "query") { + var tmpaction = { + description: parameter.description, + name: parameter.name, + required: parameter.required, + in: "query", + }; + + if (parameter.required === undefined) { + tmpaction.required = false; + } + + newaction.queries.push(tmpaction); + } else if (parameter.in === "path") { + // FIXME - parse this to the URL too + newaction.paths.push(parameter.name); + + // FIXME: This doesn't follow OpenAPI3 exactly. + // https://swagger.io/docs/specification/describing-request-body/ + // https://swagger.io/docs/specification/describing-parameters/ + // Need to split the data. + } else if (parameter.in === "body") { + // FIXME: Add tracking for components + // E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml + if (parameter.example !== undefined) { + newaction.body = parameter.example; + } + } else if (parameter.in === "header") { + newaction.headers += `${parameter.name}=${parameter.example}\n`; + } + } + + if (newaction.name === "" || newaction.name === undefined) { + // Find a unique part of the string + // FIXME: Looks for length between /, find the one where they differ + // Should find others with the same START to their path + // Make a list of reserved names? Aka things that show up only once + if (Object.getOwnPropertyNames(wordlist).length === 0) { + for (let [newpath] of Object.entries(data.paths)) { + const newpathsplit = newpath.split("/"); + for (key in newpathsplit) { + const pathitem = newpathsplit[key].toLowerCase(); + if (wordlist[pathitem] === undefined) { + wordlist[pathitem] = 1; + } else { + wordlist[pathitem] += 1; + } + } + } + } + + //console.log("WORDLIST: ", wordlist) + + // Remove underscores and make it normal with upper case etc + const urlsplit = path.split("/"); + if (urlsplit.length > 0) { + var curname = ""; + for (key in urlsplit) { + var subpath = urlsplit[key]; + if (wordlist[subpath] > 2 || subpath.length < 1) { + continue; + } + + curname = subpath; + break; + } + + // FIXME: If name exists, + // FIXME: Check if first part of parsedname is verb, otherwise use method + const parsedname = curname + .split("_") + .join(" ") + .split("-") + .join(" ") + .split("{") + .join(" ") + .split("}") + .join(" ") + .trim(); + if (parsedname.length === 0) { + newaction.errors.push("Missing name"); + } else { + const newname = + method.charAt(0).toUpperCase() + + method.slice(1) + + " " + + parsedname; + const searchactions = newActions.find( + (data) => data.name === newname + ); + //console.log("SEARCH: ", searchactions); + if (searchactions !== undefined) { + newaction.errors.push("Missing name"); + } else { + newaction.name = newname; + } + } + } else { + newaction.errors.push("Missing name"); + } + } + newActions.push(newaction); + } + + if (data.servers !== undefined && data.servers.length > 0) { + var firstUrl = data.servers[0].url; + if ( + firstUrl.includes("{") && + firstUrl.includes("}") && + data.servers[0].variables !== undefined + ) { + const regex = /{\w+}/g; + const found = firstUrl.match(regex); + if (found !== null) { + for (key in found) { + const item = found[key].slice(1, found[key].length - 1); + const foundVar = data.servers[0].variables[item]; + if (foundVar["default"] !== undefined) { + firstUrl = firstUrl.replaceAll( + found[key], + foundVar["default"] + ); + } + } + } + } + + if (firstUrl.endsWith("/")) { + setBaseUrl(firstUrl.slice(0, firstUrl.length - 1)); + } else { + setBaseUrl(firstUrl); + } + } + } + } + + // FIXME: Have multiple authentication options? + if (securitySchemes !== undefined) { + for (const [, value] of Object.entries(securitySchemes)) { + if (value.scheme === "bearer") { + setAuthenticationOption("Bearer auth"); + setAuthenticationRequired(true); + break; + } else if (value.type === "apiKey") { + setAuthenticationOption("API key"); + + value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); + setParameterLocation(value.in); + if (!apikeySelection.includes(value.in)) { + //console.log("APIKEY SELECT: ", apikeySelection) + toast("Might be error in setting up API key authentication"); + } + + //console.log("PARAM NAME: ", value.name) + setParameterName(value.name); + setAuthenticationRequired(true); + break; + } else if (value.scheme === "basic") { + setAuthenticationOption("Basic auth"); + setAuthenticationRequired(true); + break; + } + } + } + + const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") + //console.log("First actions: ", firstActions) + const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") + newActions = firstActions.concat(secondActions) + setActions(newActions); + setIsAppLoaded(true); + + if (newActions.length > 0) { + setCurrentAction(newActions[0]); + setCurrentActionMethod(newActions[0].method); + setUrlPathQueries(newActions[0].queries); + setUrlPath(newActions[0].url); + //setActionsModalOpen(true) + } + }; + + const getAppDocs = (appname, location, version) => { + if (serverside === true) { + return; + } + + fetch( + `${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, + { + headers: { + Accept: "application/json", + }, + credentials: "include", + } + ) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + //toast("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + var setMeta = false + if (responseJson.success === true) { + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + const imgRegex = / { + toast("Error in doc loading: " + error.toString()); + }); + }; + + const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => { + const index = searchClient.initIndex("appsearch"); + + console.log("Running appsearch for: ", appname); + + + index + .search(appname) + .then(({ hits }) => { + const appsearchname = appname.replaceAll("_", " ").toLowerCase(); + var found = false + + if (hits !== undefined && hits !== null && hits.length === 1) { + found = true + if (isOriginal !== false) { + handleEditApp(hits[0].objectID) + } else { + setSecondaryApp(hits[0]) + } + } + + for (var key in hits) { + const hit = hits[key]; + + if (hit["name"] === null || hit["name"] === undefined) { + continue; + } + + if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) { + /* + if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) { + var parsedtriggers = [] + for (var key in hit.triggers) { + const curtrigger = hit.triggers[key].toLowerCase() + + const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger) + if (foundTrigger !== undefined && foundTrigger !== null) { + parsedtriggers.push(foundTrigger) + } + } + + setTriggers(parsedtriggers) + } + */ + + if (triggerOnly === true) { + + } else { + if (isOriginal !== false) { + found = true + handleEditApp(hit.objectID); + } else { + console.log("Found second app: ", hit); + hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1); + setSecondaryApp(hit); + } + } + + break; + } + } + + if (!found) { + if (hits.length > 0) { + if (isOriginal !== false) { + handleEditApp(hits[0].objectID) + } else { + setSecondaryApp(hits[0]); + } + + return + } + + //navigate("/search?message=App not found&q=" + appname + "&tab=apps") + //toast("App not found. Please contact support@shuffler.io if you believe this is an error.") + return + } + }) + .catch((err) => { + console.log(err); + }); + }; + + + + + if (serverside === true && firstRequest) { + setFirstRequest(false); + if ( + selectedApp !== undefined && + selectedApp !== null && + Object.getOwnPropertyNames(selectedApp).length > 0 + ) { + parseIncomingOpenapiData(selectedApp); + } + + if ( + selectedDoc !== undefined && + selectedDoc !== null && + Object.getOwnPropertyNames(selectedDoc).length > 0 + ) { + setAppDocumentation(selectedDoc.reason); + setSelectedTab(0); + } + + if ( + secondApp !== undefined && + secondApp !== null && + Object.getOwnPropertyNames(secondApp).length > 0 + ) { + setSelectedTab(3); + setSecondaryApp(secondApp); + } + } + + //, []) + + if (serverside !== true && window.location.href !== reloadUrl) { + setReloadUrl(window.location.href); + setAppDocumentation(defaultDocs); + setTriggers([]) + //handleEditApp(params.appid) + + if (params.appid.length === 32 || params.appid.length === 36) { + handleEditApp(params.appid); + runAlgoliaAppSearch(params.appid, false, true); + } else { + runAlgoliaAppSearch(params.appid); + } + } + + const loopQueries = + urlPathQueries === undefined || + urlPathQueries === null || + urlPathQueries.length === 0 ? null : ( +
    + + Queries + {urlPathQueries.map((data, index) => { + return ( + +
    + { + //urlPathQueries[index].name = e.target.value + //setUrlPathQueries(urlPathQueries) + }} + InputProps={{ + style: { + color: "white", + }, + }} + /> +
    +
    + ); + })} + +
    + ); + + const executeSingleAction = (appid, thisaction) => { + if (serverside === true) { + return; + } + + if (isCloud) { + thisaction.environment = "Cloud" + } else { + thisaction.environment = "Shuffle" + } + + setExecutionResult({ + valid: false, + result: baseResult, + }); + + setExecuting(true); + + fetch(globalUrl + "/api/v1/apps/" + appid + "/run", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(thisaction), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + if ( + responseJson.success === true && + responseJson.result !== null && + responseJson.result !== undefined && + responseJson.result.length > 0 + ) { + const result = responseJson.result.slice(0, 50) + "..."; + //toast("SUCCESS: "+result) + + const validate = validateJson(responseJson.result); + setExecutionResult(validate); + } else if ( + responseJson.success === false && + responseJson.reason !== undefined && + responseJson.reason !== null + ) { + toast(responseJson.reason); + setExecutionResult({ valid: false, result: responseJson.reason }); + } else if (responseJson.success === true) { + setExecutionResult({ + valid: false, + result: + "Couldn't finish execution. Please fill all the required fields, and retry the execution.", + }); + } else { + setExecutionResult({ + valid: false, + result: + "Couldn't finish execution (2). Please fill all the required fields, and validate the execution.", + }); + } + + setExecuting(false); + }) + .catch((error) => { + toast("Execution error: " + error.toString()); + setExecuting(false); + }); + }; + + const MethodWrapper = (props) => { + const { data } = props; + + var bgColor = "#61afee"; + if (data.method === "POST") { + bgColor = "#49cc90"; + } else if (data.method === "PUT") { + bgColor = "#fca130"; + } else if (data.method === "PATCH") { + bgColor = "#50e3c2"; + } else if (data.method === "DELETE") { + bgColor = "#f93e3e"; + } else if (data.method === "HEAD") { + bgColor = "#9012fe"; + } + + return ( + + ); + }; + + const parseName = (name, length) => { + if (name === undefined || name === null) { + return "" + } + + var parsedName = name.charAt(0).toUpperCase() + name.slice(1); + parsedName = parsedName.replaceAll("_", " "); + if ( + length !== undefined && + length !== null && + length > 3 && + length < parsedName.length + ) { + parsedName = parsedName.slice(0, length) + ".."; + } + + return parsedName; + }; + + const SubAction = (props) => { + const { data, selected, hovered, index } = props; + + const [hoveredItem, setHoveredItem] = useState(true); + + var urlPath = data.url !== undefined && data.url !== null && data.url.length > 0 ? data.url : "" + var wrappedStyle = JSON.parse(JSON.stringify(actionListStyle)); + wrappedStyle.backgroundColor = selected || hovered ? theme.palette.platformColor : theme.palette.inputColor; + wrappedStyle.paddingBottom = urlPath.length > 0 ? 0 : 10 + wrappedStyle.border = selected || hovered ? "1px solid rgba(255,255,255,0.3)" : "" + + var methodName = `${data.method}_${data.name}`; + if ((data.name !== undefined && data.name !== null && data.method !== undefined && data.method !== null ) && (data.method.toLowerCase() === "custom" || data.name.toLowerCase().startsWith(data.method.toLowerCase()))) { + methodName = data.name; + } + + const invalid_keys = [".", "(", ")", "'", ",", "[", "]"]; + methodName = methodName.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_"); + + for (var key in invalid_keys) { + methodName = methodName.replaceAll(invalid_keys[key], ""); + } + + const parsedName = parseName(data.name, 35); + if (parsedName.length === 0) { + return null + } + + const actionLabels = foundCategory !== undefined && foundCategory !== null && foundCategory.name !== "Other" && foundCategory.action_labels.length > 0 ? ["No Label"].concat(foundCategory.action_labels) : [] + + var bgColor = "#61afee"; + if (data.method === "POST") { + bgColor = "#49cc90"; + } else if (data.method === "PUT") { + bgColor = "#fca130"; + } else if (data.method === "PATCH") { + bgColor = "#50e3c2"; + } else if (data.method === "DELETE") { + bgColor = "#f93e3e"; + } else if (data.method === "HEAD") { + bgColor = "#9012fe"; + } + + //console.log("Category: ", foundCategory, actionLabels) + + return ( + { + setHoveredItem(true); + }} + onMouseOut={() => { + setHoveredItem(false); + }} + > + {parsedName}

    {data.method} {urlPath}

    {data.description} + + } + placement="left" + > +
    { + if (app.actions !== undefined) { + var newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === methodName) + + if (newselectedaction === undefined || newselectedaction === null) { + newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === data.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "")) + if (newselectedaction === undefined || newselectedaction === null) { + for (var key in app.actions) { + console.log(methodName, app.actions[key].name.toLowerCase().replaceAll(" ", "_")); + } + + toast(`Name ${methodName} not found. Please contact us.`); + return; + } + } + + newselectedaction.app_id = app.id; + newselectedaction.app_name = app.name; + newselectedaction.app_version = app.app_version; + + newselectedaction.authentication = selectedAction.authentication; + newselectedaction.authentication_id = selectedAction.authentication_id; + newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication; + + if ( + app.authentication.required && + newselectedaction.authentication_id !== undefined && + newselectedaction.authentication_id !== null && + newselectedaction.authentication_id.length === 0 + ) { + const tmpParams = selectedAction.parameters; + selectedAction.parameters = []; + + for (var paramkey in app.authentication.parameters) { + var item = app.authentication.parameters[paramkey]; + item.configuration = true; + + const found = selectedAction.parameters.find( + (param) => param.name === item.name + ); + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + + for (var paramkey in tmpParams) { + var item = tmpParams[paramkey]; + //item.configuration = true + + const found = selectedAction.parameters.find( + (param) => param.name === item.name + ); + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + } + + setSelectedAction(newselectedaction); + } + + setCurrentAction(data); + setCurrentActionMethod(data.method); + setUrlPathQueries(data.queries); + setUrlPath(data.url); + + /* + if (selectedTab !== 1) { + setSelectedTab(1); + } + */ + }} + > +
    + + + 0 ? "auto" : 3, + marginBottom: "auto", + textAlign: "left", + overflow: "hidden", + maxHeight: 27, + maxWidth: 175, + }} + > + {parsedName} + + {urlPath.length > 0 ? + + {urlPath} + + : null} + + {actionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ? + + : null} +
    +
    +
    +
    + ); + }; + + const foundCategory = newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 ? categories.find((x) => x.name === newWorkflowCategories[0]) : undefined + const LoopActions = (props) => { + const { actions } = props; + + //const [activeActions] = useState(actions === undefined ? [] : actions); + + if (actions === undefined || actions === null || actions.length === 0) { + return null; + } + + return ( +
    + {actions.map((data, index) => { + if (data.action_label === undefined || data.action_label === null || data.action_label.length === 0) { + data.action_label = "No Label" + } + + return ( + + ); + })} +
    + ); + // + }; + + const ParsedActionHandler = () => { + const passedOrg = { id: "", name: "" }; + const owner = ""; + const passedTags = ["single test"]; + + const [, setUpdate] = useState(); + const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false); + const [selectedApp, setSelectedApp] = useState({ + versions: [ + { + id: selectedAction.app_id, + version: selectedAction.app_version, + }, + ], + loop_versions: [selectedAction.app_version], + id: selectedAction.app_id, + name: selectedAction.app_name, + version: selectedAction.app_version, + }); + + const [requiresAuthentication, setRequiresAuthentication] = useState( + app.authentication.required && + app.authentication.parameters !== undefined && + app.authentication.parameters !== null + ); + const [workflow, setWorkflow] = useState({ + name: "", + description: "", + actions: [selectedAction], + start: selectedAction.id, + tags: passedTags, + execution_org: passedOrg, + org_id: passedOrg.id, + id: uuidv4(), + isValid: true, + owner: owner, + created: Date.now(), + }); + + const EndpointData = () => { + const [tmpVar, setTmpVar] = React.useState(""); + + return ( +
    + The API endpoint to use (URL) - predefined in the app + { + setTmpVar(event.target.value); + }} + onBlur={() => { + selectedApp.link = tmpVar; + console.log("LINK: ", selectedApp.link); + setSelectedApp(selectedApp); + }} + /> +
    + ); + }; + + const setAppActionAuthentication = (newauth) => { + if (app.authentication.required) { + var findAuthId = ""; + if ( + selectedAction.authentication_id !== null && + selectedAction.authentication_id !== undefined && + selectedAction.authentication_id.length > 0 + ) { + findAuthId = selectedAction.authentication_id; + } + + var baseAuthOptions = []; + for (var key in newauth) { + var item = newauth[key]; + + const newfields = {}; + for (var filterkey in item.fields) { + newfields[item.fields[filterkey].key] = item.fields[filterkey].value; + } + + item.fields = newfields; + if (item.app.name === app.name) { + baseAuthOptions.push(item); + + if (item.id === findAuthId) { + selectedAction.selectedAuthentication = item; + } + } + } + + selectedAction.authentication = baseAuthOptions; + //console.log("Authentication: ", authenticationOptions) + if ( + selectedAction.selectedAuthentication === null || + selectedAction.selectedAuthentication === undefined || + selectedAction.selectedAuthentication.length === "" + ) { + selectedAction.selectedAuthentication = {}; + } + } else { + selectedAction.authentication = []; + selectedAction.authentication_id = ""; + selectedAction.selectedAuthentication = {}; + } + + setSelectedAction(selectedAction); + console.log("Action: ", selectedAction); + }; + + //{selectedAction.authentication !== undefined && selectedAction.authentication.length > 0 ? + const getAppAuthentication = () => { + if (serverside === true) { + return; + } + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success && responseJson.data !== undefined && responseJson.data !== null && responseJson.data.length !== 0) { + var newauth = []; + + //console.log("Got auth. Trying to map to selectedAction appname: ", selectedAction) + var authUpdate = false; + const appname = selectedAction.app_name.toLowerCase().replaceAll(" ", "_") + selectedAction.authentication = [] + + for (var key in responseJson.data) { + if (responseJson.data[key].defined === false) { + continue; + } + + if (responseJson.data[key].active === false) { + continue; + } + + newauth.push(responseJson.data[key]); + + if (responseJson.data[key].app === undefined || responseJson.data[key].app === null) { + continue + } + + if (responseJson.data[key].app.name.toLowerCase().replaceAll(" ", "_") === appname) { + console.log("Found matching app name: ", responseJson.data[key].app.name) + selectedAction.authentication.push(responseJson.data[key]) + selectedAction.authentication_id = responseJson.data[key].id + selectedAction.selectedAuthentication = responseJson.data[key] + authUpdate = true; + } +} + + console.log("New auth: ", newauth) + if (authUpdate === true) { + setSelectedAction(selectedAction) + } + + //setUpdate(Math.random()) + setAppAuthentication(newauth); + setAppActionAuthentication(newauth); + } else { + if (app.authentication.required) { + const tmpParams = selectedAction.parameters; + selectedAction.parameters = []; + + for (var paramkey in app.authentication.parameters) { + var item = app.authentication.parameters[paramkey]; + item.configuration = true; + + const found = selectedAction.parameters.find( + (param) => param.name === item.name + ); + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + + for (var paramkey in tmpParams) { + var item = tmpParams[paramkey]; + //item.configuration = true + + const found = selectedAction.parameters.find((param) => param.name === item.name); + + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + + setSelectedAction(selectedAction); + } + + //toast("Failed getting authentications") + } + }) + .catch((error) => { + toast("Auth loading error: " + error.toString()); + }); + }; + + if (!authLoaded && appAuthentication.length === 0 && selectedAction.id !== undefined) { + setAuthLoaded(true); + getAppAuthentication(); + } else if ( + selectedAction.id === undefined && + currentAction.name !== undefined && + currentAction.name !== null && + currentAction.name.length > 0 + ) { + var methodName = `${currentAction.method}_${currentAction.name}`; + if ( + currentAction.method.toLowerCase() === "custom" || + currentAction.name + .toLowerCase() + .startsWith(currentAction.method.toLowerCase()) + ) { + methodName = currentAction.name; + } + + methodName = methodName.toLowerCase().replaceAll(" ", "_"); + if (app.actions !== null && app.actions !== undefined) { + var newselectedaction = app.actions.find( + (item) => item.name.toLowerCase().replaceAll(" ", "_") === methodName + ); + if (newselectedaction !== undefined && newselectedaction !== null) { + newselectedaction.app_id = app.id; + newselectedaction.app_name = app.name; + newselectedaction.app_version = app.app_version; + newselectedaction.authentication = []; + newselectedaction.authentication_id = ""; + newselectedaction.selectedAuthentication = {}; + setSelectedAction(newselectedaction); + } + } + } + + const setNewAppAuth = (appAuthData) => { + if (serverside === true) { + return; + } + + //console.log("DAta: ", appAuthData) + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to set app auth: " + responseJson.reason); + } else { + getAppAuthentication(true); + setAuthenticationModalOpen(false); + + // Needs a refresh with the new authentication.. + //toast("Successfully saved new app auth") + } + }) + .catch((error) => { + toast("Auth error: ", error.toString()); + }); + }; + + const AuthenticationData = (props) => { + const selectedApp = props.app; + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if (selectedApp.authentication === undefined) { + return null; + } + + if ( + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return null; + } + + authenticationOption.app.actions = []; + + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = ""; + } + } + + const handleSubmitCheck = () => { + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + //toast("Label can't be empty") + //return + } + + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ].length === 0 + ) { + toast( + "Field " + + selectedApp.authentication.parameters[key].name + + " can't be empty" + ); + return; + } + } + + console.log("Action: ", selectedAction); + selectedAction.authentication_id = authenticationOption.id; + selectedAction.selectedAuthentication = authenticationOption; + if ( + selectedAction.authentication === undefined || + selectedAction.authentication === null + ) { + selectedAction.authentication = [authenticationOption]; + } else { + selectedAction.authentication.push(authenticationOption); + } + + setSelectedAction(selectedAction); + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (const key in newAuthOption.fields) { + const value = newAuthOption.fields[key]; + newFields.push({ + key: key, + value: value, + }); + } + + console.log("FIELDS: ", newFields); + newAuthOption.fields = newFields; + setNewAppAuth(newAuthOption); + //appAuthentication.push(newAuthOption) + //setAppAuthentication(appAuthentication) + // + + setUpdate(authenticationOption.id); + + /* + {selectedAction.authentication.map(data => ( + + */ + }; + + if ( + authenticationOption.label === null || + authenticationOption.label === undefined + ) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
    + + + What is this? + +
    + These are required fields for authenticating with {selectedApp.name} +
    + Name - what is this used for? + { + authenticationOption.label = event.target.value; + }} + /> + {selectedApp.link.length > 0 ? ( +
    + +
    + ) : null} + +
    + {selectedApp.authentication.parameters !== undefined && + selectedApp.authentication.parameters !== null + ? selectedApp.authentication.parameters.map((data, index) => { + return ( +
    + + {data.name} + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> +
    + ); + }) + : null} + + + + + +
    + ); + }; + + const authenticationModal = authenticationModalOpen ? ( + { + //setAuthenticationModalOpen(false) + }} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + color: "white", + minWidth: 600, + padding: 15, + }, + }} + > + { + setAuthenticationModalOpen(false); + }} + > + + + +
    + Authentication for {selectedApp.name} +
    +
    + + {/**/} + + {app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ? + + : + + } +
    + ) : null; + + const selectedNameChange = (event) => { + if (event.target === undefined || event.target === null || event.target.value === undefined || event.target.value === null || event.target.value.length === 0) { + return + } + + + //console.log("OLDNAME: ", selectedAction.name) + event.target.value = event.target.value.replaceAll("(", ""); + event.target.value = event.target.value.replaceAll(")", ""); + event.target.value = event.target.value.replaceAll("$", ""); + event.target.value = event.target.value.replaceAll("#", ""); + event.target.value = event.target.value.replaceAll(".", ""); + event.target.value = event.target.value.replaceAll(",", ""); + event.target.value = event.target.value.replaceAll(" ", "_"); + selectedAction.label = event.target.value; + setSelectedAction(selectedAction); + }; + + const actionStyling = { + width: "100%", + }; + + //backgroundColor: "#1F2023", + const appApiViewStyle = { + display: "flex", + flexDirection: "column", + color: "white", + minHeight: "100%", + zIndex: 1000, + resize: "vertical", + overflowY: "auto", + overflowX: "hidden", + maxHeight: 680, + paddingRight: 7, + }; + // maxWidth: 420, + + return ( + + {authenticationModal} + + {selectedAction.id !== undefined ? ( + + ) : null} + + ); + }; + + const SelectedActionView = (props) => { + const { action } = props; + //console.log("Parsedaction: ", selectedAction) + const parsedName = parseName(action.name); + const splitHeaders = action.headers === undefined || action.headers === null ? [] : action.headers.split("\n"); + + return ( +
    +
    + + + {parsedName} + +
    + + +
    + ); + }; + + + const AppDetails = (props) => { + const { title, inputTitle } = props + + const [details, setDetails] = useState("") + + + return ( + + { + setDetails(event.target.value) + }} + onBlur={(event) => { + submitAppDetails(title.toLowerCase().replaceAll(" ", "_"), details) + }} + /> + + ) + } + + const imageStyle = { + borderRadius: theme.palette?.borderRadius, + border: "1px solid rgba(255,255,255,0.6)", + minWidth: 100, + maxWidth: 100, + minhHight: 100, + maxHeight: 100, + }; + + const textStyle = { + marginLeft: 15, + marginTop: 15, + }; + + const submitAppDetails = (field, value) => { + console.log("To submit. Skipping if value is empty: ", field, value) + + if (value === undefined || value === null || (value.length === 0 && field !== "triggers")) { + return + } + + //toast("Submitting details for field", field) + const data = { + field: field, + value: value, + app_id: app.id, + } + + fetch(globalUrl + "/api/v1/apps/label", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason === undefined) { + toast("Failed to submit details for field ", field, " because ", responseJson.reason) + } else { + toast("Failed to submit details for field ", field, " because ", responseJson.reason) + } + } else { + toast("Successfully submitted details for field ", field) + } + }) + .catch((error) => { + console.log("Error: ", error) + toast("Error submitting details for field ", field) + }) + } + + const editTrigger = (triggerName) => { + console.log("Editing trigger: ", triggerName) + + var found = false + for (var i = 0; i < triggers.length; i++) { + if (triggers[i].name === triggerName) { + found = true + break + } + } + + var newtriggers = JSON.parse(JSON.stringify(triggers)) + if (!found) { + // Find the trigger with the same name in imported workflowTriggers + var importedTrigger = undefined + for (var i = 0; i < workflowTriggers.length; i++) { + if (workflowTriggers[i].name.toLowerCase() === triggerName.toLowerCase()) { + newtriggers.push(workflowTriggers[i]) + break + } + } + + } else { + // Remove + newtriggers = newtriggers.filter((trigger) => trigger.name !== triggerName) + } + + console.log("Triggers: ", newtriggers) + setTriggers(newtriggers) + + var parsedtriggers = [] + for (var i = 0; i < newtriggers.length; i++) { + parsedtriggers.push(newtriggers[i].name) + } + + submitAppDetails("triggers", parsedtriggers.join(",")) + } + + const removeAppFromSearchEngine = (appID) => { + toast(`Removing app ${appId} from search engine`) + + const field = "public" + const data = { + field: field, + value: "false", + app_id: appID, + } + + fetch(globalUrl + "/api/v1/apps/label", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason === undefined) { + toast.error("Failed removing app from search engine.") + } else { + toast.error("Failed to remove app from search engine because: " + responseJson.reason) + } + } else { + toast.info("Successfully unpublished app. It can still be accessed by direct link.") + } + }) + .catch((error) => { + console.log("Error: ", error) + toast.info("Error when removing app from search engine.") + }) + } + + const deduplicateByName = (array) => { + const uniqueNames = {}; + return array.filter(item => { + if (!item?.hasOwnProperty('name') || !item?.name?.length) { + return true + } + if (!uniqueNames[item.name]) { + uniqueNames[item.name] = true + return true + } + return false + }) + } + + const ActionSelectOption = (actionprops) => { + const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops; + const [hover, setHover] = React.useState(false); + + return ( + +
    setHover(true)} onMouseLeave={() => setHover(false)} + onClick={(event) => { + console.log("Clicked on action: ", option) + + + if (option !== undefined && option !== null) { + setSelectedValidationAction(option) + + const labelData = { + "app_id": app.id, + "action_name": option.name, + "label": "app_validation", + } + + // Should send recommendations to the owner + var url = `${globalUrl}/api/v1/apps/label`; + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + body: JSON.stringify(labelData), + credentials: "include", + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + toast(responseJson.reason) + } + }) + .catch((error) => { + console.log("Error: ", error) + }) + + // Update the app itself? + /* + setNewSelectedAction({ + target: { + value: option.name + } + }); + */ + } + }} + > +
    + + {useIcon} + + {newActionname} +
    + {extraDescription.length > 0 ? + + {extraDescription} + + : null} +
    +
    + ) + } + + const userRoles = ["you", "public"]; + + const updateAppField = (app_id, fieldname, fieldvalue) => { + const data = {}; + data[fieldname] = fieldvalue; + + + console.log("DATA: ", data); + + fetch(globalUrl + "/api/v1/apps/" + app_id, { + method: "PATCH", + headers: { + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + //setAppSearchLoading(false) + return response.json(); + }) + .then((responseJson) => { + //console.log(responseJson) + //toast(responseJson) + if (responseJson.success) { + toast("Successfully updated app configuration"); + } else { + if (responseJson.reason !== undefined && responseJson.reason !== null) { + toast("Error: "+responseJson.reason); + } else { + toast("Error updating app configuration. Are you the owner of this app?"); + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const sortByCategoryLabel = (a, b) => { + const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 + const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 + + // Sort by existence and length of "category_label" + if (aHasCategoryLabel && !bHasCategoryLabel) { + return -1 + } else if (!aHasCategoryLabel && bHasCategoryLabel) { + return 1 + } else { + return 0 + } + } + + const getDownloadUrl = () => { + + console.log("APP: ", app) + + const appEnding = app?.public === true ? app?.app_version : app?.id + + return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip` + } + + const renderedActionOptions = deduplicateByName(( + actions === undefined || actions === null ? [] : + actions.filter((a) => + a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(actions, "label")) + ).sort(sortByCategoryLabel)) + + const actionView = + actions === undefined || actions === null ? null : ( +
    +
    + + {isMobile || appType === 0 || appType === 2 ? null : ( + +
    + +
    +
    + )} + + + { + if (newValue === 1 && appType === 0 || appType === 2) { + window.open(`/apis/${app.id}`, "_blank") + } else { + setSelectedTab(newValue) + } + }} + style={{ marginBottom: 0, marginLeft: 0, marginRight: 0, minWidth: 800, maxWidth: 800, margin: "auto", }} + aria-label="disabled tabs example" + > + } label="Docs" /> + : } label={appType === 0 || appType === 2 ? "Explore the API" : "Try it out"} /> + + } label="Stats" /> + } disabled label="Integrations" /> + } disabled={userdata.support !== true} label="Creator" value={4} /> + +
    + {selectedTab === 1 && app.skipped_build == false ? ( +
    +
    + +
    +
    + {currentAction.description !== undefined && currentAction.description !== null && currentAction.description !== "" ? +
    + + Action Description + + + {currentAction.description} + +
    + : app.description !== undefined && + app.description !== null && + app.description.length > 0 ? +
    + + App Description + + + {app.description} + +
    + : null} +
    + + Result + + { + executeSingleAction( + selectedAction.app_id, + selectedAction + ); + }} + > + + + + +
    + {executing ? ( +
    + {serverside === true ? null : + + } +
    + ) : ( +
    + {executionResult.valid ? ( + { + handleReactJsonClipboard(copy); + }} + onSelect={(select) => { + HandleJsonCopy( + executionResult.result, + select, + "exec" + ); + console.log("SELECTED!: ", select); + }} + name={"Result"} + /> + ) : ( + + {executionResult.result} + + )} +
    + )} +
    +
    + ) : selectedTab === 2 && app.id !== undefined ? ( +
    + + + Use the App onprem (hybrid) + + + Due to using docker containers with privately uploaded containers, we had to use a custom registry. Use the command below to download the image to the server if it fails to run. + + It will authenticate and authorize you, before redirecting to a Signed URL on https://storage.googleapis.com + +  Now also works for ARM containers! + + +
    + + {getDownloadUrl()} + +
    + + +
    + +
    +
    + ) : selectedTab === 0 ? ( +
    + + {appDocumentation} + +
    + ) : selectedTab === 3 && secondaryApp.objectID !== undefined && app.name !== undefined ? ( +
    + + Connect {app.name.replaceAll("_", " ")} and{" "} + {secondaryApp.name.replaceAll("_", " ")} + + + Using Shuffle, you can connect{" "} + {app.name.replaceAll("_", " ")} and{" "} + {secondaryApp.name.replaceAll("_", " ")} with no code. + +
    +
    + + + {app.name.replaceAll("_", " ")} + +
    +
    + + + {secondaryApp.name.replaceAll("_", " ")} + +
    + +
    +
    + + + +
    + + + +
    + + {appDocumentation} + +
    +
    + ) : + selectedTab === 4 ? +
    + + App Details + + + Add more details about your app here. This is to help both the Shuffle team, and the public get easier access to this information. Data from these will be used to track app "completeness" for recommendation systems. + + + + Validation Action + + + The validation action is the action that is used to validate the app. This is used both when a user wants to validate their auth, as well as when Shuffle runs automatic tests of the app. It is recommended that the action should be a GET request. Validation is decided based on whether the action is ran successfully in a workflow. + + + { + // Most popular + // Is categorized + // Uncategorized + return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; + }} + renderGroup={(params) => { + + return ( +
  • + {params.group} + {params.children} +
  • + ) + }} + options={renderedActionOptions} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + }, + }} + filterOptions={(options, { inputValue }) => { + const lowercaseValue = inputValue === null ? "" : inputValue.toLowerCase() + options = options.filter((x) => { + if (x.name === undefined || x.name === null) { + x.name = "" + } + + if (x.description === undefined || x.description === null) { + x.description = "" + } + + if (x.method !== "GET") { + return null + } + + return x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue) + }) + + return options + }} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.name === undefined || option.name === null ) { + return null; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + + return newname; + }} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + // Workaround with event lol + console.log("Changed to: ", event, newValue) + toast("Changed validation action") + if (newValue !== undefined && newValue !== null) { + /* + setNewSelectedAction({ + target: { + value: newValue.name + } + }) + */ + } + }} + renderOption={(props, option, state) => { + var newActionname = option.name; + if (option.label !== undefined && option.label !== null && option.label.length > 0) { + newActionname = option.label; + } + + var newActiondescription = option.description; + //console.log("DESC: ", newActiondescription) + if (option.description === undefined || option.description === null) { + newActiondescription = "Description: No description defined for this action" + } else { + newActiondescription = "Description: "+newActiondescription + } + + const iconInfo = GetIconInfo({ name: option.name }); + const useIcon = iconInfo.originalIcon; + + if (newActionname === undefined || newActionname === null) { + newActionname = "No name" + option.name = "No name" + option.label = "No name" + } + + newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); + + var method = "" + var extraDescription = "" + if (option.name.includes("get_")) { + method = "GET" + } else if (option.name.includes("post_")) { + method = "POST" + } else if (option.name.includes("put_")) { + method = "PUT" + } else if (option.name.includes("patch_")) { + method = "PATCH" + } else if (option.name.includes("delete_")) { + method = "DELETE" + } else if (option.name.includes("options_")) { + method = "OPTIONS" + } else if (option.name.includes("connect_")) { + method = "CONNECT" + } + + // FIXME: Should it require a base URL? + if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) { + var extraUrl = "" + const descSplit = option.description.split("\n") + // Last line of descSplit + if (descSplit.length > 0) { + extraUrl = descSplit[descSplit.length-1] + } + + if (extraUrl.length > 0) { + if (extraUrl.includes(" ")) { + extraUrl = extraUrl.split(" ")[0] + } + + if (extraUrl.includes("#")) { + extraUrl = extraUrl.split("#")[0] + } + + extraDescription = `${method} ${extraUrl}` + } else { + //console.log("No url found. Check again :)") + } + } + + return ( + + ); + }} + renderInput={(params) => { + if (params.inputProps?.value) { + const prefixes = ["Post", "Put", "Patch"]; + for (let prefix of prefixes) { + if (params.inputProps.value.startsWith(prefix)) { + let newValue = params.inputProps.value.replace(prefix + " ", ""); + if (newValue.length > 1) { + newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1); + } + // Set the new value without mutating inputProps + params = { ...params, inputProps: { ...params.inputProps, value: newValue } }; + break; + } + } + // Check if it starts with "Get List" and method is "Get" + if (params.inputProps.value.startsWith("Get List")) { + console.log("Get List") + } + } + + const actionDescription = "" + const isIntegration = false + + return ( + + + + ) + }} + /> + + + + Triggers + +
    + + Schedule: + + trigger.name === "Schedule") !== undefined} + label="Schedule" + onChange={(e) => { + editTrigger("Schedule") + + }} + /> + + Webhook: + + trigger.name === "Webhook") !== undefined} + label="Webhook" + onChange={() => { + editTrigger("Webhook") + + }} + /> +
    + {triggers === undefined || triggers === null || triggers.find(trigger => trigger.name === "Webhook") === undefined ? null : + { + submitAppDetails("extra_value", e.target.value) + }} + /> + } + + + External info + + + + + + + Partner Details + + + + + + + + Public Status + + + +
    + : + ( +
    + + This app is currently in Beta, but is usable. Interested in using this app? Click the button below or contact us. +
    + {activateButton} +
    +
    + + {app.description !== undefined && + app.description !== null && + app.description !== "" ? + + + More about the app + + + {app.description} + + + : null} +
    + )} +
    +
    +
    +
    + ); + + // Random names for type & autoComplete. Didn't research :^) + const imageData = file.length > 0 ? file : fileBase64; + const height = 100; + const imageInfo = ( + + ); + + const publishModal = publishModalOpen ? ( + { + setPublishModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: '500px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} + > + +
    + Are you sure you want to PUBLISH this app? +
    +
    + +
    + + Before publishing, make sure to sanitize the App for anything you don't want public. + + + The published App is yours, and you can always change your public Apps after they are released. + +
    + + +
    +
    + ) : null; + + const landingpageDataBrowser = ( +
    + {publishModal} +
    + {isMobile ? null : ( + + +

    + + Apps +

    + + +

    {name}

    + +
    + )} +
    + {app.documentation_download_url !== undefined && + app.documentation_download_url !== null && + app.documentation_download_url.length > 0 ? ( + { + const data = openapi; + + let linkElement = document.createElement("a"); + linkElement.setAttribute("target", "_blank"); + linkElement.setAttribute( + "href", + app.documentation_download_url + ); + linkElement.setAttribute( + "download", + app.documentation_download_url + ); + linkElement.click(); + }} + > + + + + + ) : null} + {appType === 0 || appType === 2 ? ( + { + const data = openapi; + let exportFileDefaultName = name + ".json"; + + let dataStr = JSON.stringify(data); + let dataUri = + "data:application/json;charset=utf-8," + + encodeURIComponent(dataStr); + let linkElement = document.createElement("a"); + linkElement.setAttribute("href", dataUri); + linkElement.setAttribute("download", exportFileDefaultName); + linkElement.click(); + + const tmpurl = new URL(window.location.href); + const searchParams = tmpurl.searchParams; + const queryID = searchParams.get("queryID"); + + if (queryID !== undefined && queryID !== null) { + aa("init", { + appId: "JNSS5CFDZZ", + apiKey: "db08e40265e2941b9a7d8f644b6e5240", + }); + + const timestamp = new Date().getTime(); + aa("sendEvents", [ + { + eventType: "conversion", + eventName: "Public App Downloaded", + index: "appsearch", + objectIDs: [app.id], + timestamp: timestamp, + queryID: queryID, + userToken: + userdata === undefined || + userdata === null || + userdata.id === undefined + ? "unauthenticated" + : userdata.id, + }, + ]); + } else { + console.log("No query to handle when downloading"); + } + }} + > + + + + + ) : null} + + {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined && userdata.support === true ? + // Iconbutton for authentication with just an icon. Link to /apps/authentication?app_id=app.id + { + window.open(`/appauth?app_id=${app.id}&auth=${selectedOrganization.org_auth.token}`, "_blank") + }} + > + + + + + : null} + + {isMobile || userdata?.active_apps === undefined || userdata?.active_apps === null || !userdata?.active_apps?.includes(appId) ? null : + + } + + {isMobile ? null : ( + + )} + {appType === 1 ? null : + creatorProfile.self === true || userdata.support === true ? + + + + : isMobile || !isLoggedIn ? null : + + + + } + + {appType === 0 || appType === 2 ? ( +
    + + + + {(appdata?.owner === userdata?.id) ? ( + + ): null } +
    + ) : ( + + + + )} +
    +
    +
    + +
    +
    { + upload.click(); + }} + > + {imageInfo} +
    +
    +
    + + {name} + + + {(app.public || appType === 0) && app.skipped_build == false ? ( + + + + ) : ( + + + + )} + + + {/* Handles category changing, but looks like shit. Should probably work as a suggestion */} + {/* + + */} +
    +
    + + Version - {app?.app_version} + +
    +
    + {Object.getOwnPropertyNames(creatorProfile).length !== 0 && + creatorProfile.github_avatar !== undefined && + creatorProfile.github_avatar !== null ? ( +
    + { + setAnchorElAvatar(event.currentTarget); + }} + > + + + + + + Shared by{" "} + + {creatorProfile.github_username} + + + {contact.name !== undefined && + contact.name !== null && + !contact.name.includes("frikky") && + contact.name.length > 0 && + contact.name.toLowerCase() !== + creatorProfile.github_username.toLowerCase() && + !( + contact.name.toLowerCase().includes("anon") && + creatorProfile.github_username.length > 0 + ) ? ( + + {" "} + •     Created by {contact.name} + + ) : ( + "" + )} + +
    + ) : contact.name !== undefined && + contact.name !== null && + contact.name.length > 0 ? ( + + Created by {contact.name} + + ) : null} +
    + {newWorkflowTags.map((tag, index) => { + return ( + + ); + })} +
    +
    +
    + + {isMobile || serverside ? null : ( + + +
    + + {relatedWorkflows !== 0 ? ( + relatedWorkflows + ) : ( + + + + + )} + + + Workflows + +
    +
    +
    + + )} + {app.video !== undefined && + app.video !== null && + app.video.includes("http") ? ( +
    + {app.video.includes("loom.com/share") && + app.video.split("/").length > 4 ? ( +
    +