Merge pull request #1802 from Shuffle/nightly

Merge 2.1.0 into main
This commit is contained in:
Frikky
2025-08-28 21:49:51 +02:00
committed by GitHub
107 changed files with 6891 additions and 4402 deletions
-1
View File
@@ -1,5 +1,4 @@
# Default execution environment for workers
ORG_ID=Shuffle
ENVIRONMENT_NAME=Shuffle
# Sanitize liquid.py input
+1 -1
View File
@@ -18,7 +18,7 @@ The Docker setup is the default setup, and is ran with docker compose. This is [
**PS: if you're setting up Shuffle on Windows, go to the next step (Windows Docker setup)**
1. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed, and that you have a minimum of **2Gb of RAM** available.
1. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed, and that you have a minimum of **4Gb of RAM** available.
2. Download Shuffle
```bash
git clone https://github.com/Shuffle/Shuffle
+31
View File
@@ -0,0 +1,31 @@
name: Helm Test
on:
push:
paths:
- "functions/kubernetes/charts/**"
workflow_dispatch:
permissions:
contents: read
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: Update helm dependencies
run: helm dependency update ./functions/kubernetes/charts/shuffle
- name: Template helm chart using default values
run: helm template shuffle ./functions/kubernetes/charts/shuffle --debug --values ./functions/kubernetes/charts/shuffle/values.yaml
@@ -0,0 +1,92 @@
name: Tagged Nightly Release
on:
release:
types: [published]
branches:
- nightly
# there is a very clear point to this existing.
# we want to also release versions that look like this:
# v2.1.0-nightly-date, v2.1.0-nightly-date-1, v2.1.0-nightly-date-2
# we NEVER want to send customers a "nightly" tag. We always want to send them
# a tagged nightly tag. So that when something breaks, They can always
# point to it.
jobs:
main:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- app: frontend
path: frontend
experimental: true
- app: backend
path: backend
experimental: true
- app: app_sdk
path: backend/app_sdk
experimental: true
- app: orborus
path: functions/onprem/orborus
experimental: true
- app: worker
path: functions/onprem/worker
experimental: true
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set version
id: set_version
run: |
if [[ ${{ github.event_name }} == 'release' ]]; then
echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
else
echo "VERSION=nightly-untagged-latest" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: "amd64,arm64,arm"
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Ghcr
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Ghcr Build and push
id: docker_build
uses: docker/build-push-action@v4
env:
BUILDX_NO_DEFAULT_LOAD: true
with:
logout: false
context: ${{ matrix.path }}/
file: ${{ matrix.path }}/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
frikky/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
frikky/shuffle:${{ matrix.app }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+1 -1
View File
@@ -27,7 +27,7 @@ FROM alpine:latest as certs
RUN apk add --update ca-certificates
# Sets up the final image
FROM alpine:3.21.2
FROM alpine:3.22.1
# FIXME: Install cgo because CGO_ENABLED=1 during build
RUN apk add --no-cache libc6-compat
+18 -15
View File
@@ -4,8 +4,8 @@ go 1.24.0
toolchain go1.24.3
//replace github.com/frikky/schemaless => ../../../schemaless
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
//replace github.com/frikky/schemaless => ../../../schemaless
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
@@ -14,17 +14,19 @@ require (
cloud.google.com/go/storage v1.55.0
github.com/basgys/goxml2json v1.1.0
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/docker/docker v28.2.2+incompatible
github.com/docker/docker v28.3.3+incompatible
github.com/frikky/kin-openapi v0.42.0
github.com/fsouza/go-dockerclient v1.12.1
github.com/ghodss/yaml v1.0.0
github.com/go-co-op/gocron v1.37.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.1
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.84
golang.org/x/crypto v0.39.0
github.com/shuffle/shuffle-shared v0.9.14
github.com/shuffle/singul v0.0.16
golang.org/x/crypto v0.40.0
google.golang.org/api v0.236.0
google.golang.org/grpc v1.72.2
gopkg.in/yaml.v3 v3.0.1
@@ -55,7 +57,6 @@ require (
github.com/bitly/go-simplejson v0.5.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
@@ -72,7 +73,7 @@ require (
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/frikky/schemaless v0.0.16 // indirect
github.com/frikky/schemaless v0.0.20 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
@@ -114,18 +115,21 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opensearch-project/opensearch-go v1.1.0 // indirect
github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect
github.com/osteele/liquid v1.7.0 // indirect
github.com/osteele/tuesday v1.0.3 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/sashabaranov/go-openai v1.40.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sashabaranov/go-openai v1.40.5 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
@@ -135,19 +139,18 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
+43 -26
View File
@@ -126,8 +126,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -152,8 +152,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0=
github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4=
github.com/frikky/schemaless v0.0.16 h1:4d2ZktB9xGsAusbbKliOI8TuriSrdIMzD/6ToY3wkz8=
github.com/frikky/schemaless v0.0.16/go.mod h1:jT48kTcmr1q3o8i+8qe7g+eCsbwaz2Q9CjOJevQQzQs=
github.com/frikky/schemaless v0.0.20 h1:S/A2pQcRN9qa2RnufvxwCeM06trjG0JLTF3urt1tFQI=
github.com/frikky/schemaless v0.0.20/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY=
github.com/fsouza/go-dockerclient v1.12.1 h1:FMoLq+Zhv9Oz/rFmu6JWkImfr6CBgZOPcL+bHW4gS0o=
github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -162,6 +162,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
@@ -243,6 +245,7 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAx
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
@@ -278,6 +281,7 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -325,10 +329,15 @@ github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
github.com/osteele/liquid v1.7.0 h1:VsbPSchE5D5S5scylAIvERET4dnCxsO6IDri2oSJ5Dk=
github.com/osteele/liquid v1.7.0/go.mod h1:xU0Z2dn2hOQIEFEWNmeltOmCtfhtoW/2fCyiNQeNG+U=
github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU=
github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -336,12 +345,16 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU=
github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.40.5 h1:SwIlNdWflzR1Rxd1gv3pUg6pwPc6cQ2uMoHs8ai+/NY=
github.com/sashabaranov/go-openai v1.40.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
@@ -350,8 +363,10 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM=
github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk=
github.com/shuffle/shuffle-shared v0.9.14 h1:POkTHO+bByuv8HiKuCMSGgtpDKk86ISr6ooLG8vQfuE=
github.com/shuffle/shuffle-shared v0.9.14/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E=
github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs=
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=
@@ -359,8 +374,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -417,8 +432,10 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -428,8 +445,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
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=
@@ -478,8 +495,8 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
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=
@@ -495,8 +512,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -521,14 +538,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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=
@@ -539,8 +556,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -572,8 +589,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+118 -34
View File
@@ -3,6 +3,7 @@ package main
import (
uuid "github.com/satori/go.uuid"
"github.com/shuffle/shuffle-shared"
"github.com/shuffle/singul/pkg"
"archive/zip"
"bufio"
@@ -62,6 +63,7 @@ var registryName = "registry.hub.docker.com"
var runningEnvironment = "onprem"
var syncUrl = "https://shuffler.io"
var debug = false
//var syncUrl = "http://localhost:5002"
type retStruct struct {
@@ -319,7 +321,7 @@ func checkError(cmdName string, cmdArgs []string) error {
scanner := bufio.NewScanner(cmdReader)
go func() {
for scanner.Scan() {
fmt.Printf("Out: %s\n", scanner.Text())
log.Printf("Out: %s\n", scanner.Text())
}
}()
@@ -638,7 +640,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
} else {
log.Printf("[DEBUG] Successfully created the default org!")
defaultEnv := os.Getenv("ORG_ID")
defaultEnv := os.Getenv("ENVIRONMENT_NAME")
if len(defaultEnv) == 0 {
defaultEnv = "Shuffle"
log.Printf("[DEBUG] Setting default environment for org to %s", defaultEnv)
@@ -1068,6 +1070,74 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
activatedAppIds = append(activatedAppIds, app.ID)
}
parsedStatus := []string{}
if len(org.ManagerOrgs) > 0 || userInfo.ActiveOrg.CreatorOrg != "" {
parsedStatus = append(parsedStatus, "sub_org")
parentOrgId := userInfo.ActiveOrg.CreatorOrg
if len(userInfo.ActiveOrg.CreatorOrg) == 0 {
parentOrgId = org.ManagerOrgs[0].Id
}
// Check for licensing/branding of parent and override
parentOrg, err := shuffle.GetOrg(ctx, parentOrgId)
if err == nil {
if parentOrg.LeadInfo.IntegrationPartner {
parsedStatus = append(parsedStatus, "integration_partner")
// except theme take from parent org
org.Branding.EnableChat = parentOrg.Branding.EnableChat
org.Branding.HomeUrl = parentOrg.Branding.HomeUrl
org.Branding.DocumentationLink = parentOrg.Defaults.DocumentationReference
org.Branding.SupportEmail = parentOrg.Branding.SupportEmail
org.Branding.LogoutUrl = parentOrg.Branding.LogoutUrl
org.Branding.BrandColor = parentOrg.Branding.BrandColor
org.Branding.BrandName = parentOrg.Branding.BrandName
org.Branding.GlobalUser = parentOrg.Branding.GlobalUser
if len(org.Branding.Theme) == 0 {
org.Branding.Theme = parentOrg.Branding.Theme
}
userInfo.ActiveOrg.Branding = parentOrg.Branding
userInfo.ActiveOrg.Image = parentOrg.Image
userInfo.ActiveOrg.Branding.DocumentationLink = parentOrg.Defaults.DocumentationReference
userInfo.ActiveOrg.Branding.BrandColor = parentOrg.Branding.BrandColor
userInfo.ActiveOrg.Branding.SupportEmail = org.Branding.SupportEmail
userInfo.ActiveOrg.Branding.LogoutUrl = org.Branding.LogoutUrl
userInfo.ActiveOrg.Branding.BrandName = parentOrg.Branding.BrandName
if len(org.Branding.Theme) == 0 {
userInfo.ActiveOrg.Branding.Theme = parentOrg.Branding.Theme
} else {
userInfo.ActiveOrg.Branding.Theme = org.Branding.Theme
}
// check whether current is global user or not? means is user part of parent org or not
for _, user := range parentOrg.Users {
if user.Id == userInfo.Id && user.Role == "admin" {
userInfo.ActiveOrg.Branding.GlobalUser = true
break
}
}
}
}
} else {
// for parent org branding
if org.LeadInfo.IntegrationPartner {
userInfo.ActiveOrg.Branding.Theme = org.Branding.Theme
userInfo.ActiveOrg.Branding.DocumentationLink = org.Defaults.DocumentationReference
userInfo.ActiveOrg.Branding.SupportEmail = org.Branding.SupportEmail
userInfo.ActiveOrg.Branding.LogoutUrl = org.Branding.LogoutUrl
userInfo.ActiveOrg.Branding.BrandColor = org.Branding.BrandColor
userInfo.ActiveOrg.Branding.BrandName = org.Branding.BrandName
parsedStatus = append(parsedStatus, "integration_partner")
}
}
aiEnabled := os.Getenv("OPENAI_API_URL") != "" && os.Getenv("AI_MODEL") != ""
returnValue := shuffle.HandleInfo{
Success: true,
Username: userInfo.Username,
@@ -1091,6 +1161,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
Licensed: licensed,
ActiveApps: activatedAppIds,
Theme: userInfo.Theme,
OrgStatus: parsedStatus,
AIEnabled: aiEnabled,
}
returnData, err := json.Marshal(returnValue)
@@ -1330,7 +1402,7 @@ func parseWorkflowParameters(resp http.ResponseWriter, request *http.Request) (m
return t, err
}
//fmt.Println(curjson.String())
//log.Println(curjson.String())
//log.Printf("Parsing json a second time: %s", string(curjson.String()))
err = json.Unmarshal(curjson.Bytes(), &t)
@@ -2484,7 +2556,7 @@ func execSubprocess(cmdName string, cmdArgs []string) error {
scanner := bufio.NewScanner(cmdReader)
go func() {
for scanner.Scan() {
fmt.Printf("Out: %s\n", scanner.Text())
log.Printf("Out: %s\n", scanner.Text())
}
}()
@@ -3456,7 +3528,7 @@ func handleAppHotload(ctx context.Context, location string, forceUpdate bool) er
return err
}
_, _, err = IterateAppGithubFolders(ctx, fs, dir, "", "", forceUpdate)
_, _, err = IterateAppGithubFolders(ctx, fs, dir, "", "", forceUpdate, false)
if err != nil {
log.Printf("[WARNING] Githubfolders error: %s", err)
return err
@@ -3909,7 +3981,7 @@ func runInitCloudSetup() {
if err != nil {
log.Printf("[INFO] Failed initial setup: %s", err)
} else {
log.Printf("[INFO] Ran initial setup!")
log.Printf("[INFO] Finished initial cloudsync setup!")
}
}
@@ -3925,7 +3997,7 @@ func runInitEs(ctx context.Context) {
log.Printf("[INFO] Running with HTTPS proxy %s (env: HTTPS_PROXY)", httpsProxy)
}
defaultEnv := os.Getenv("ORG_ID")
defaultEnv := os.Getenv("ENVIRONMENT_NAME")
if len(defaultEnv) == 0 {
defaultEnv = "Shuffle"
log.Printf("[DEBUG] Setting default environment for org to %s", defaultEnv)
@@ -4031,8 +4103,9 @@ func runInitEs(ctx context.Context) {
time.Sleep(30 * time.Second)
}
// FIXME: This should ONLY run on one backend instance
shuffle.InitOpensearchIndexes()
// FIXME: This should ONLY run on one backend instance. This may cause interference.
schedules, err := shuffle.GetAllSchedules(ctx, "ALL")
if err != nil {
log.Printf("[WARNING] Failed getting schedules during service init: %s", err)
@@ -4078,14 +4151,23 @@ func runInitEs(ctx context.Context) {
//log.Printf("Schedule: %#v", schedule)
//log.Printf("Schedule time: every %d seconds", schedule.Seconds)
jobret, err := newscheduler.Every(schedule.Seconds).Seconds().NotImmediately().Run(job(schedule))
if err != nil {
log.Printf("[ERROR] Failed to start schedule for workflow %s: %s", schedule.WorkflowId, err)
if schedule.Seconds == 0 && len(schedule.Frequency) > 0 {
cronJob, err := CronScheduler.Cron(schedule.Frequency).Do(job(schedule))
if err != nil {
log.Printf("[ERROR] Failed to start schedule for workflow %s: %s", schedule.WorkflowId, err)
} else {
log.Printf("[DEBUG] Successfully started schedule for workflow %s", schedule.WorkflowId)
}
cronJobs[schedule.Id] = cronJob
} else {
log.Printf("[DEBUG] Successfully started schedule for workflow %s", schedule.WorkflowId)
jobret, err := newscheduler.Every(schedule.Seconds).Seconds().NotImmediately().Run(job(schedule))
if err != nil {
log.Printf("[ERROR] Failed to start schedule for workflow %s: %s", schedule.WorkflowId, err)
} else {
log.Printf("[DEBUG] Successfully started schedule for workflow %s", schedule.WorkflowId)
}
scheduledJobs[schedule.Id] = jobret
}
scheduledJobs[schedule.Id] = jobret
}
}
@@ -4337,20 +4419,8 @@ func runInitEs(ctx context.Context) {
}
// Getting apps to see if we should initialize a test
// FIXME: Isn't this a little backwards?
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
log.Printf("[INFO] Getting and validating workflowapps. Got %d with err %#v", len(workflowapps), err)
// accept any certificate (might be useful for testing)
//customGitClient := &http.Client{
// Transport: &http.Transport{
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// },
// Timeout: 15 * time.Second,
//}
//client.InstallProtocol("http", githttp.NewClient(customGitClient))
//client.InstallProtocol("https", githttp.NewClient(customGitClient))
if err != nil && len(workflowapps) == 0 {
log.Printf("[WARNING] Failed getting apps (runInit): %s", err)
} else if err == nil && len(workflowapps) < 10 {
@@ -4360,8 +4430,9 @@ func runInitEs(ctx context.Context) {
url := os.Getenv("SHUFFLE_APP_DOWNLOAD_LOCATION")
if len(url) == 0 {
log.Printf("[INFO] Skipping download of apps since no URL is set. Default would be https://github.com/shuffle/shuffle-apps")
url = "https://github.com/shuffle/shuffle-apps"
log.Printf("[INFO] Skipping download of apps since no URL is set. Default would be https://github.com/shuffle/python-apps")
url = "https://github.com/shuffle/python-apps"
//url = ""
//return
}
@@ -4401,7 +4472,7 @@ func runInitEs(ctx context.Context) {
_ = r
//iterateAppGithubFolders(fs, dir, "", "testing")
_, _, err = IterateAppGithubFolders(ctx, fs, dir, "", "", forceUpdate)
_, _, err = IterateAppGithubFolders(ctx, fs, dir, "", "", forceUpdate, true)
if err != nil {
log.Printf("[WARNING] Error from app load in init: %s", err)
}
@@ -4409,6 +4480,10 @@ func runInitEs(ctx context.Context) {
// Hotloads locally
location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER")
if len(location) == 0 {
location = "./shuffle-apps"
}
if len(location) != 0 {
handleAppHotload(ctx, location, false)
}
@@ -5090,6 +5165,7 @@ func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) {
func initHandlers() {
var err error
ctx := context.Background()
CronScheduler.StartAsync()
log.Printf("[DEBUG] Starting Shuffle backend - initializing database connection")
//requestCache = cache.New(5*time.Minute, 10*time.Minute)
@@ -5176,11 +5252,13 @@ func initHandlers() {
r.HandleFunc("/api/v1/workflows/queue", handleGetWorkflowqueue).Methods("GET", "POST")
r.HandleFunc("/api/v1/workflows/queue/confirm", handleGetWorkflowqueueConfirm).Methods("POST")
// App specific
// From here down isnt checked for org specific
// App specific. Partially Singul.
r.HandleFunc("/api/v1/apps/categories", shuffle.GetActiveCategories).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/categories/run", singul.RunCategoryAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{key}/execute", executeSingleAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{key}/run", executeSingleAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/categories", shuffle.GetActiveCategories).Methods("GET", "OPTIONS")
//r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}/activate", activateWorkflowAppDocker).Methods("GET", "OPTIONS")
@@ -5248,6 +5326,8 @@ func initHandlers() {
// First v2 API
r.HandleFunc("/api/v2/workflows/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v2/workflows/generate/llm", shuffle.HandleWorkflowGenerationResponse).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v2/workflows/edit/llm", shuffle.HandleEditWorkflowWithLLM).Methods("POST", "OPTIONS")
// New for recommendations in Shuffle
r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS")
@@ -5308,8 +5388,8 @@ func initHandlers() {
// EVERYTHING below here is NEW for 0.8.0 (written 25.05.2021)
r.HandleFunc("/api/v1/workflows/{key}/publish", makeWorkflowPublic).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS")
//r.HandleFunc("/api/v1/orgs", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS")
//r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleGetOrg).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleEditOrg).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgid}/forms", shuffle.HandleGetOrgForms).Methods("GET", "OPTIONS")
@@ -5404,6 +5484,10 @@ func initHandlers() {
// Had to move away from mux, which means Method is fucked up right now.
func main() {
if os.Getenv("DEBUG") == "true" {
debug = true
}
initHandlers()
hostname, err := os.Hostname()
if err != nil {
+135 -205
View File
@@ -11,7 +11,7 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
//"math/rand"
"net/http"
"net/url"
"os"
@@ -26,6 +26,7 @@ import (
uuid "github.com/satori/go.uuid"
newscheduler "github.com/carlescere/scheduler"
"github.com/go-co-op/gocron"
"github.com/frikky/kin-openapi/openapi3"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
@@ -42,17 +43,22 @@ var baseEnvironment = "onprem"
var cloudname = "cloud"
var scheduledJobs = map[string]*newscheduler.Job{}
var cronJobs = map[string]*gocron.Job{}
var scheduledOrgs = map[string]*newscheduler.Job{}
var CronScheduler = gocron.NewScheduler(time.UTC)
// Frequency = cronjob OR minutes between execution
func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode, frequency, orgId string, body []byte) error {
var err error
testSplit := strings.Split(frequency, "*")
cronJob := ""
isCron := false
newfrequency := 0
if len(testSplit) > 5 {
cronJob = frequency
isCron = true
} else {
newfrequency, err = strconv.Atoi(frequency)
if err != nil {
@@ -65,12 +71,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
//} else if int(newfrequency) <
}
// Reverse. Can't handle CRON, only numbers
if len(cronJob) > 0 {
return errors.New("cronJob isn't formatted correctly")
}
if newfrequency < 1 {
if newfrequency < 1 && !isCron {
return errors.New("Frequency has to be more than 0")
}
@@ -96,17 +97,27 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
}
}
log.Printf("[INFO] Starting frequency for execution: %d", newfrequency)
log.Printf("[INFO] Starting frequency for execution: %s", frequency)
//jobret, err := newscheduler.Every(newfrequency).Seconds().NotImmediately().Run(job)
jobret, err := newscheduler.Every(newfrequency).Seconds().Run(job)
if err != nil {
log.Printf("Failed to schedule workflow: %s", err)
return err
if isCron {
cronJob, err := CronScheduler.Cron(cronJob).Do(job)
if err != nil {
log.Printf("[ERROR] Failed to start schedule with cron(%s): %s", cronJob, err)
}
cronJobs[scheduleId] = cronJob
} else {
//jobret, err := newscheduler.Every(newfrequency).Seconds().NotImmediately().Run(job)
jobret, err := newscheduler.Every(newfrequency).Seconds().Run(job)
if err != nil {
log.Printf("Failed to schedule workflow: %s", err)
return err
}
scheduledJobs[scheduleId] = jobret
}
//scheduledJobs = append(scheduledJobs, jobret)
scheduledJobs[scheduleId] = jobret
// Doesn't need running/not running. If stopped, we just delete it.
timeNow := int64(time.Now().Unix())
@@ -117,6 +128,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
Argument: string(body),
WrappedArgument: bodyWrapper,
Seconds: newfrequency,
Frequency: frequency,
CreationTime: timeNow,
LastModificationtime: timeNow,
LastRuntime: timeNow,
@@ -245,7 +257,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
return
}
// This is really the environment's name - NOT org-id
// This is really the environment's name - NOT OrgId
environment := request.Header.Get("Org-Id")
if len(environment) == 0 {
log.Printf("[AUDIT] No org-id header set")
@@ -254,7 +266,8 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
return
}
orgId := request.Header.Get("org")
// Org => Org ID here
orgId := request.Header.Get("Org")
if len(orgId) == 0 {
//log.Printf("[AUDIT] No 'org' header set (get workflow queue). ")
/*
@@ -263,9 +276,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
return
*/
}
orborusLabel := request.Header.Get("x-orborus-label")
// This section is cloud custom for now
auth := request.Header.Get("Authorization")
if len(auth) == 0 {
@@ -277,192 +288,46 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
*/
}
//log.Printf("[AUDIT] Get workflow queue for org %s, env %s, orborus label %s", orgId, environment, orborusLabel)
ctx := shuffle.GetContext(request)
// Get all env and check the name?
envs, err := shuffle.GetEnvironments(ctx, orgId)
if err != nil || len(envs) == 0 {
log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", environment, err)
//log.Printf("[WARNING] No env found for orgId %s during queue loading", orgId)
}
var env *shuffle.Environment
for _, e := range envs {
if e.Name == environment {
env = &e
found := false
for i := range envs {
if envs[i].Name == environment {
env = &envs[i]
found = true
break
}
}
timeNow := time.Now().Unix()
err = shuffle.HandleOrborusFailover(ctx, request, resp, env)
if err != nil {
if !strings.Contains(err.Error(), "mismatch") {
log.Printf("[WARNING] Failed handling Orborus failover: %s", err)
// Only works onprem - shared queues across tenants
// without tenancy
if !found {
env, err = shuffle.GetEnvironment(ctx, environment, "")
if err != nil {
log.Printf("[WARNING] Failed to find the environment(%s) in org(%s). Could cause with Failover test", environment, orgId)
}
return
}
//log.Printf("Found env: %#v", env)
// Handles failover control between Orborus'
// Further tracks checkin time to ensure this works properly
// across instances
err = shuffle.HandleOrborusFailover(ctx, request, resp, env)
if err != nil {
log.Printf("[WARNING] Failed handling Orborus failover: %s", err)
}
if len(env.OrgId) > 0 {
orgId = env.OrgId
}
// FIXME: Workflow stats disabled for now
// as it caused too many problems
// goal: track docker stuff once a minute and graph it
// For now: Disable this as it caused too many problems
if request.Method == "POST" && true == false {
//log.Printf("[DEBUG] POST to workflowqueue")
if rand.Intn(10) == 0 {
// Parse out body
body, err := ioutil.ReadAll(request.Body)
if err == nil {
// Parse out CPU, memory and disk.
var envData shuffle.OrborusStats
err = json.Unmarshal(body, &envData)
if err == nil && !envData.Swarm && !envData.Kubernetes && (envData.CPU > 0 || envData.Memory > 0 || envData.Disk > 0) {
// Set the input in memory
envData.OrgId = orgId
envData.Environment = environment
envData.OrborusLabel = orborusLabel
envData.Timestamp = time.Now().Unix()
if envData.CPU > 0 && envData.MaxCPU > 0 {
envData.CPUPercent = float64(envData.CPU) / float64(envData.MaxCPU)
}
if envData.Memory > 0 && envData.MaxMemory > 0 {
envData.MemoryPercent = float64(envData.Memory) / float64(envData.MaxMemory)
}
// Check if CPU percent constantly has stayed above X% for the last Y requests
percentageCheck := 90
concurrentChecks := 2
//if int(envData.CPUPercent) > percentageCheck {
// Get cached data
percentages := []float64{}
cacheKey := fmt.Sprintf("%s_%s_percent", environment , strings.ToLower(orgId))
// Marshal float list into []byte
cacheData := []byte{}
cache, err := shuffle.GetCache(ctx, cacheKey)
if err == nil {
// Unmarshal into percentages
cacheData := []byte(cache.([]uint8))
err = json.Unmarshal(cacheData, &percentages)
if err != nil {
log.Printf("[INFO] error in cache unmarshal for percentages: %s", err)
}
if len(percentages) > concurrentChecks {
percentages = percentages[:concurrentChecks]
}
percentages = append(percentages, envData.CPUPercent)
if len(percentages) > concurrentChecks {
//log.Printf("[INFO] Checking percentages: %v", percentages)
// percentageCheck := 1
sendAlert := true
for _, p := range percentages {
if int(p) < percentageCheck {
//log.Printf("[AUDIT] CPU percent is below %d: %d", percentageCheck, int(p))
sendAlert = false
break
}
}
if sendAlert {
log.Printf("[INFO] CPU percent has been above %d percent for the last 5 requests. Sending alert. Env: %s, org: %s", percentageCheck, environment, orgId)
// Set notification + alert for organization
err = shuffle.CreateOrgNotification(
ctx,
fmt.Sprintf("CPU percent has been above %d percent", percentageCheck),
fmt.Sprintf("A environment %s has been using more than %d\\% CPU for the last 5 requests.", environment, percentageCheck),
fmt.Sprintf("/admin?tab=environments"),
environment,
true,
)
if err != nil {
log.Printf("[ERROR] error creating notification: %s", err)
}
org, err := shuffle.GetOrg(ctx, environment)
if err == nil {
foundRecommendation := false
for _, recommendation := range org.Priorities {
if strings.Contains(recommendation.Name, "CPU") {
foundRecommendation = true
break
}
}
if !foundRecommendation {
// Add to start of org.Priorities
org, _ = shuffle.AddPriority(*org, shuffle.Priority{
Name: fmt.Sprintf("High CPU in environment %s", orgId),
Description: fmt.Sprintf("The environment %s has been using more than %d percent CPU. This indicates you may need to look at scaling.", orgId, percentageCheck),
Type: "scale",
Active: true,
URL: fmt.Sprintf("/admin?tab=environments"),
Severity: 1,
}, false)
//Make last item the first item
org.Priorities = append([]shuffle.Priority{org.Priorities[len(org.Priorities)-1]}, org.Priorities[:len(org.Priorities)-1]...)
err = shuffle.SetOrg(ctx, *org, org.Id)
if err != nil {
log.Printf("[ERROR] Problem setting org: %s", err)
}
}
}
}
if len(percentages) > 1 {
percentages = percentages[1:]
}
}
// Marshal float list into []byte
} else {
//log.Printf("[ERROR] Failed getting cache: %s", err)
percentages = append(percentages, envData.CPUPercent)
}
if len(percentages) > 0 {
//log.Printf("[DEBUG] Setting cache for %s: %#v", cacheKey, percentages)
cacheData, err = json.Marshal(percentages)
if err != nil {
log.Printf("[INFO] error in cache marshal: %s", err)
}
// Add the new data
go shuffle.SetCache(ctx, cacheKey, cacheData, 5)
}
}
//log.Printf("CPU percent: %f", envData.CPUPercent)
//log.Printf("Memory percent: %f", envData.MemoryPercent*100)
go shuffle.SetenvStats(ctx, envData)
}
}
}
executionRequests, err := shuffle.GetWorkflowQueue(ctx, environment, 100)
if err != nil {
// Skipping as this comes up over and over
//log.Printf("(2) Failed reading body for workflowqueue: %s", err)
resp.WriteHeader(401)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
@@ -471,10 +336,11 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
if len(executionRequests.Data) == 0 {
executionRequests.Data = []shuffle.ExecutionRequest{}
} else {
//log.Printf("In workflowqueue with %d", len(executionRequests.Data))
// Try again :)
// Try again? I don't think this is necessary, and shouldn't really ever occur.
/*
if len(env.Id) == 0 && len(env.Name) == 0 {
timeNow := int64(time.Now().Unix())
foundId := ""
for _, requestData := range executionRequests.Data {
execution, err := shuffle.GetWorkflowExecution(ctx, requestData.ExecutionId)
@@ -487,9 +353,10 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
}
if len(environment) > 0 {
env, err := shuffle.GetEnvironment(ctx, foundId, environment)
env, err := shuffle.GetEnvironment(ctx, environment, foundId)
if err != nil {
log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", orgId, err)
log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", environment, err)
//resp.WriteHeader(401)
//resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No env found matching %s"}`, id)))
//return
@@ -505,6 +372,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
}
}
}
*/
if len(executionRequests.Data) > 50 {
executionRequests.Data = executionRequests.Data[0:49]
@@ -1685,7 +1553,15 @@ func deleteSchedule(ctx context.Context, id string) error {
value.Lock()
} else {
// FIXME - allow it to kind of stop anyway?
return errors.New("Can't find the schedule.")
if j, ok := cronJobs[id]; ok {
err := CronScheduler.RemoveByID(j)
if err != nil {
log.Printf("[ERROR] Failed to remove the scheduler %s", err)
return err
}
} else {
return errors.New("Can't find the schedule.")
}
}
}
@@ -2141,14 +2017,14 @@ func validateAppInput(resp http.ResponseWriter, request *http.Request) {
//fmt.Printf("File type: %s. MIME: %s\n", kind.Extension, kind.MIME.Value)
if kind == filetype.Unknown {
fmt.Println("Unknown file type")
log.Println("Unknown file type")
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if kind.MIME.Value != "application/zip" {
fmt.Println("Not zip, can't unzip")
log.Println("Not zip, can't unzip")
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
@@ -2307,17 +2183,24 @@ func handleSingleAppHotloadRequest(resp http.ResponseWriter, request *http.Reque
resp.Write([]byte(`{"success": false}`))
return
}
if user.Role != "admin" {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "Must be admin to hotload apps"}`))
return
}
location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER")
if len(location) == 0 {
location = "./shuffle-apps"
}
if len(location) == 0 {
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SHUFFLE_APP_HOTLOAD_FOLDER not specified in .env"}`)))
return
}
requestUrlFields := strings.Split(request.URL.String(), "/")
var appName string
if requestUrlFields[1] == "api" {
@@ -2381,6 +2264,10 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) {
}
location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER")
if len(location) == 0 {
location = "./shuffle-apps"
}
if len(location) == 0 {
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SHUFFLE_APP_HOTLOAD_FOLDER not specified in .env"}`)))
@@ -2982,24 +2869,24 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
}
location := strings.Split(request.URL.String(), "/")
var fileId string
var appId string
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false}`))
return
}
fileId = location[4]
appId = location[4]
}
//log.Printf("[AUDIT] User Authentication failed in execute SINGLE action - CONTINUING ANYWAY: %s. Found OrgID: %#v", err, user.ActiveOrg.Id)
log.Printf("[AUDIT] User %s (%s) in org %s (%s) is running SINGLE App run for App ID '%s'", user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id, fileId)
log.Printf("[AUDIT] User %s (%s) in org %s (%s) is running SINGLE App run for App ID '%s'", user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id, appId)
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[INFO] Failed single execution POST body read: %s", err)
resp.WriteHeader(401)
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false}`))
return
}
@@ -3024,7 +2911,15 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
decisionId = decision[0]
}
workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, fileId, body, runValidationAction, decisionId)
log.Printf("\n\nACTION TO RUN: %s. Body: %s. Source URL: %s\n\n", appId, string(body), request.URL.String())
workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, appId, body, runValidationAction, decisionId)
if appId == "agent_starter" {
log.Printf("[INFO] Returning early for agent_starter single action execution: %s", workflowExecution.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)))
return
}
debugUrl := fmt.Sprintf("/workflows/%s?execution_id=%s", workflowExecution.Workflow.ID, workflowExecution.ExecutionId)
resp.Header().Add("X-Debug-Url", debugUrl)
@@ -3065,6 +2960,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
go shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions")
executionRequest := shuffle.ExecutionRequest{
Priority: 15,
ExecutionId: workflowExecution.ExecutionId,
WorkflowId: workflowExecution.Workflow.ID,
Authorization: workflowExecution.Authorization,
@@ -3110,7 +3006,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
}
// Onlyname is used to
func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string, forceUpdate bool) ([]shuffle.BuildLaterStruct, []shuffle.BuildLaterStruct, error) {
func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string, forceUpdate, duringStartup bool) ([]shuffle.BuildLaterStruct, []shuffle.BuildLaterStruct, error) {
var err error
allapps := []shuffle.WorkflowApp{}
@@ -3122,7 +3018,14 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
"YARA",
"ATTACK-PREDICTOR",
}
//if strings.ToUpper(workflowapp.Name) == strings.ToUpper(appname) {
startupNames := []string{
"shuffle-tools",
"http",
"email",
"shuffle-ai",
"shuffle-subflow",
}
// It's here to prevent getting them in every iteration
buildLaterFirst := []shuffle.BuildLaterStruct{}
@@ -3132,6 +3035,19 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
continue
}
//duringStartup
if duringStartup {
// Look for names: shuffle tools, http, email, shuffle ai
if shuffle.ArrayContains(startupNames, strings.ToLower(file.Name())) {
// Allowed to build during startup
//log.Printf("\n\n\nFOUND MATCHING APP: %s\n\n\n", file.Name())
} else {
//log.Printf("\n\n\nWRONG APP (2): %s\n\n\n", file.Name())
continue
}
}
// Folder?
switch mode := file.Mode(); {
case mode.IsDir():
@@ -3148,7 +3064,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
}
// Go routine? Hmm, this can be super quick I guess
buildFirst, buildLast, err := IterateAppGithubFolders(ctx, fs, dir, tmpExtra, "", forceUpdate)
buildFirst, buildLast, err := IterateAppGithubFolders(ctx, fs, dir, tmpExtra, "", forceUpdate, false)
for _, item := range buildFirst {
buildLaterFirst = append(buildLaterFirst, item)
@@ -3160,7 +3076,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
if err != nil {
log.Printf("[WARNING] Error reading folder: %s", err)
//buildFirst, buildLast, err := IterateAppGithubFolders(fs, dir, tmpExtra, "", forceUpdate)
//buildFirst, buildLast, err := IterateAppGithubFolders(fs, dir, tmpExtra, "", forceUpdate, false)
if !forceUpdate {
continue
@@ -3460,6 +3376,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
"http",
"email",
}
for _, buildLater := range buildLaterFirst {
found := false
for _, appname := range initApps {
@@ -3478,12 +3395,19 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
}
// Prepend newSortedList to buildLaterFirst
handledImages := []string{}
buildLaterFirst = append(newSortedList, buildLaterFirst...)
if len(extra) == 0 {
log.Printf("[INFO] Starting build of %d containers (FIRST)", len(buildLaterFirst))
for _, item := range buildLaterFirst {
if len(item.Tags) > 0 && shuffle.ArrayContains(handledImages, item.Tags[0]) {
continue
}
handledImages = append(handledImages, item.Tags[0])
err = buildImageMemory(fs, item.Tags, item.Extra, true)
if err != nil {
orgId := ""
@@ -3513,6 +3437,12 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
if len(buildLaterList) > 0 {
log.Printf("[INFO] Starting build of %d skipped docker images", len(buildLaterList))
for _, item := range buildLaterList {
if len(item.Tags) > 0 && shuffle.ArrayContains(handledImages, item.Tags[0]) {
continue
}
handledImages = append(handledImages, item.Tags[0])
err = buildImageMemory(fs, item.Tags, item.Extra, true)
if err != nil {
log.Printf("[INFO] Failed image build memory: %s", err)
@@ -3646,7 +3576,7 @@ func LoadSpecificApps(resp http.ResponseWriter, request *http.Request) {
}
}
IterateAppGithubFolders(ctx, fs, dir, "", "", tmpBody.ForceUpdate)
IterateAppGithubFolders(ctx, fs, dir, "", "", tmpBody.ForceUpdate, false)
} else if strings.Contains(tmpBody.URL, "s3") {
//https://docs.aws.amazon.com/sdk-for-go/api/service/s3/
+1 -1
View File
@@ -61,7 +61,7 @@ services:
security_opt:
- seccomp:unconfined
opensearch:
image: opensearchproject/opensearch:3.0.0
image: opensearchproject/opensearch:3.2.0
hostname: shuffle-opensearch
container_name: shuffle-opensearch
environment:
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

+39 -6
View File
@@ -92,7 +92,7 @@ const App = (message, props) => {
const [dataset, setDataset] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const [curpath, setCurpath] = useState(typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname)
const { themeMode, handleThemeChange, setBrandColor, brandColor,setThemeMode} = useContext(Context);
const { themeMode, handleThemeChange, setBrandColor, brandColor,setThemeMode, setSupportEmail, setLogoutUrl, setBrandName} = useContext(Context);
const currentTheme = getTheme(themeMode, brandColor);
const mainColor = currentTheme?.palette?.backgroundColor
const [isPreviousThemeLight, setIsPreviousThemeLight] = useState(false)
@@ -192,11 +192,44 @@ const App = (message, props) => {
{ path: "/" }
);
}
if (responseJson?.theme?.length > 0) {
handleThemeChange(responseJson.theme)
}else{
handleThemeChange("dark")
}
if (responseJson?.org_status?.includes("integration_partner")) {
if (responseJson?.active_org?.branding?.enable_chat !== true) {
// Find the drift chatbox and remove it
}
if (responseJson?.active_org?.branding?.theme?.length > 0 ) {
handleThemeChange(responseJson?.active_org?.branding?.theme)
}
if (responseJson?.active_org?.branding?.brand_color?.length > 0 ) {
setBrandColor(responseJson?.active_org?.branding?.brand_color)
localStorage.setItem("brandColor", responseJson?.active_org?.branding?.brand_color)
}
if (responseJson?.active_org?.branding?.brand_name?.length > 0 ) {
setBrandName(responseJson?.active_org?.branding?.brand_name)
localStorage.setItem("brandName", responseJson?.active_org?.branding?.brand_name)
}
if (responseJson?.active_org?.branding?.support_email?.length > 0 ) {
setSupportEmail(responseJson?.active_org?.branding?.support_email)
}
if (responseJson?.active_org?.branding?.logout_url?.length > 0 ) {
setLogoutUrl(responseJson?.active_org?.branding?.logout_url)
}
} else {
localStorage.removeItem("brandColor")
setBrandColor("#ff8544")
setBrandName("Shuffle")
localStorage.removeItem("brandName")
if(responseJson?.theme?.length > 0){
handleThemeChange(responseJson?.theme)
}else{
handleThemeChange("dark")
}
}
}else {
handleThemeChange("dark")
setThemeMode("dark")
+11 -5
View File
@@ -92,9 +92,14 @@ const AdminNavBar = (props) => {
const HandleVisibleTabs = () => {
if (userdata?.id?.length > 0) {
if (userdata?.active_org?.role === "admin" || userdata?.support) {
setVisibleItems(items);
if (isChildOrg) {
const filteredItems = items.filter(item => item.text !== "Partner");
setVisibleItems(filteredItems);
}else {
setVisibleItems(items);
}
}else {
const filteredItems = items.filter(item => item.text !== "Users" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations");
const filteredItems = items.filter(item => item.text !== "Users" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations" && item.text !== "Partner");
setVisibleItems(filteredItems);
}
}
@@ -105,12 +110,12 @@ const AdminNavBar = (props) => {
// Filter out Users and Tenants tabs
if (userdata?.active_org?.role === "admin" || userdata?.support) {
const filteredItems = items.filter(item =>
item.text !== "Users" && item.text !== "Tenants"
item.text !== "Users" && item.text !== "Tenants" && item.text !== "Partner"
);
setVisibleItems(filteredItems);
}else {
const filteredItems = items.filter(item =>
item.text !== "Users" && item.text !== "Tenants" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations"
item.text !== "Users" && item.text !== "Partner" && item.text !== "Tenants" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations"
);
setVisibleItems(filteredItems);
}
@@ -191,7 +196,7 @@ const AdminNavBar = (props) => {
const params = new URLSearchParams(location.search);
const tab = params?.get('tab')?.toLowerCase();
if (tab === "users" || tab === "tenants") {
if (tab === "users" || tab === "tenants" || tab === "partner") {
toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab.");
setTimeout(() => {
setSelectedItem("Organization");
@@ -200,6 +205,7 @@ const AdminNavBar = (props) => {
}
, 3000);
}
} else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams?.get('admin_tab')?.toLowerCase();
-1
View File
@@ -2,7 +2,6 @@
// 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';
+186 -60
View File
@@ -91,18 +91,21 @@ const Billing = memo((props) => {
const [currentTab, setCurrentTab] = useState(0)
const [allChildOrgs, setAllChildOrgs] = useState([])
const [allChildOrgsStats, setAllChildOrgsStats] = useState([])
const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
useEffect(() => {
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100;
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
setCurrentAppRunsInPercentage(Math.round(percentage));
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage);
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage - userdata.app_executions_suborgs);
}
if (userdata?.id?.length > 0 && isLoggedIn === false){
setIsLoggedIn(true)
}
}, [userdata]);
}, [monthlyAppRunsParent, monthlyAllSuborgExecutions, userdata]);
const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email);
@@ -199,6 +202,48 @@ const Billing = memo((props) => {
}
}, [])
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgid}/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
}
setStatistics(responseJson);
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
if (selectedOrganization && selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id);
}
}, [selectedOrganization]);
const paperStyle = {
padding: 20,
// maxWidth: 400,
@@ -1860,19 +1905,6 @@ const Billing = memo((props) => {
const updateAlertThreshold = (index, field, value) => {
if (field === 'percentage') {
if (value > 100 || value < 0) {
value = 0
toast("The percentage value should be between 0 and 100")
}
} else if (field === 'count') {
if (value < 0 || value >= userdata.app_execution_limit) {
value = 0
toast("The count value should be greater than 0 and less than the total app execution limit")
}
}
const totalValue = userdata.app_execution_limit;
const newAlertThresholds = alertThresholds.map((threshold, i) => {
if (i === index) {
@@ -1986,7 +2018,13 @@ const Billing = memo((props) => {
};
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
useEffect(() => {
if (isChildOrg && currentTab === 0) {
setCurrentTab(1);
}
}, [isChildOrg, currentTab]);
return (
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
<div style={{ height: "100%", width: "100%"}}>
@@ -2386,8 +2424,8 @@ const Billing = memo((props) => {
<Typography variant="body2" color="textSecondary" style={{fontSize: 16,}}>
We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at {supportEmail}.
</Typography>We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 5, }}>
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
<div style={{ display: 'flex', width: '50%', flexDirection: 'row', marginTop: 5, }}>
{/* {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
isChildOrg ? null : (
<ConsultationManagement
globalUrl={globalUrl}
@@ -2395,7 +2433,7 @@ const Billing = memo((props) => {
selectedOrganization={selectedOrganization}
/>
)
) : null}
) : null} */}
<TrainingService />
</div>
</div>
@@ -2423,9 +2461,21 @@ const Billing = memo((props) => {
}}
/>
<Typography style={{marginTop: 10, fontSize: 16,}} color="textSecondary">
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs.
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs this month.
</Typography>
{userdata?.active_org?.creator_org?.length > 0 ? null :
(
<>
<Typography color="textSecondary" style={{ marginTop: 20, fontSize: 16 }}>
Parent Organization App Executions: <strong>{monthlyAppRunsParent}</strong>
</Typography>
<Typography color="textSecondary" style={{ fontSize: 16 }}>
Sub-Organization App Executions: <strong>{monthlyAllSuborgExecutions || "N/A"}</strong>
</Typography>
</>
)}
<div>
<Typography style={{ marginTop: 20, fontSize: 18 }}>
Set email alert thresholds for app runs
@@ -2442,8 +2492,8 @@ const Billing = memo((props) => {
: " " + 0 + " "}
app runs.
</Typography>
<Typography color="textSecondary" style={{ fontSize: 16 }}>
<span style={{fontWeight: 'bold'}}>Please note</span>: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification.
<Typography color="textSecondary" style={{ fontSize: 16, marginTop: 10 }}>
<span style={{fontWeight: 'bold'}}>Please note</span>: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification. For Parent organizations, the alert will be sent base on the total app runs from both parent and sub-organizations. For Sub-organizations, the alert will be sent based on the app runs of the sub-organization only.
</Typography>
<div style={{ marginTop: 15 }}>
{alertThresholds.map((threshold, index) => (
@@ -2603,12 +2653,7 @@ const Billing = memo((props) => {
<Tabs
value={currentTab}
onChange={(event, newValue) => {
setCurrentTab(-1)
// Force re-render
setTimeout(() => {
setCurrentTab(newValue)
}, 100);
setCurrentTab(newValue)
}}
style={{ marginTop: 20 }}
TabIndicatorProps={{
@@ -2620,52 +2665,66 @@ const Billing = memo((props) => {
}
}}
>
<Tab
label="Parent Organization"
{isChildOrg ? null :
<Tab
label="All Organization Stats"
style={{ textTransform: 'none',}}
value={0}
/>}
<Tab
label={isChildOrg ? "Organization Stats" : "Parent Organization Stats"}
style={{ textTransform: 'none',}}
value={1}
/>
{isCloud ?
<Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={1}
/>
: null}
{isChildOrg ? null :
<Tab
label="Child Organization Stats"
disabled={isChildOrg}
style={{ textTransform: 'none', }}
value={2}
/>
/>}
{isCloud ?
<Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={3}
/>
: null}
</Tabs>
<div style={{paddingBottom: 200, minHeight: 750, }}>
{currentTab === 0 ?
<div style={{ marginTop: 30,}}>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
/>
</div>
{
currentTab === 0 ?
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
statistics={statistics}
monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/>
: currentTab === 1 ?
<div style={{ marginTop: 30,}}>
<div>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
syncStats={true}
statistics={statistics}
monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/>
</div>
:
: currentTab === 2 ?
<BillingStatsChildOrg
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
@@ -2676,7 +2735,18 @@ const Billing = memo((props) => {
setAllChildOrgs={setAllChildOrgs}
allChildOrgsStats={allChildOrgsStats}
setAllChildOrgsStats={setAllChildOrgsStats}
currentTab={currentTab}
/>
:
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
currentTab={currentTab}
syncStats={true}
/>
}
</div>
</span>
@@ -2706,6 +2776,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
const { themeMode, brandColor, supportEmail } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
// Handle page change
const handleChangePage = (event, newPage) => {
setPage(newPage);
@@ -2830,6 +2901,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
usage: stat?.monthly_app_executions || "N/A",
workflows_usage: stat?.total_workflow_executions || "N/A",
workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A",
app_runs_hard_limit: subOrg?.Billing?.app_runs_hard_limit || 0,
}
})
@@ -2890,6 +2962,33 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
</>
)}
},
{
field: "app_runs_hard_limit", headerName: "App Executions Hard Limit", width: 200, renderCell: (params) => {
console.log("params.row: ", params.row)
return (
<>
<Typography style={{ fontSize: 16 }}>
{params.row.app_runs_hard_limit}
</Typography>
<IconButton
style={{ color: theme.palette.primary.main }}
onClick={() => {
setOpen(true)
setEditingOrgId(params.row.orgId)
setEditing("app_executions_hard_limit")
if (params.row.app_runs_hard_limit === "N/A") {
setLimit("")
} else {
setLimit(params.row.app_runs_hard_limit)
}
}}
>
<Edit/>
</IconButton>
</>
)
}
}
]
setSubOrgStatsColumns(columns)
@@ -2913,7 +3012,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
return
}
if (selectedOrganization.sync_features.app_executions.limit <= 10000) {
if (selectedOrganization.sync_features.app_executions.limit <= 10000 && editing === "app_executions") {
toast.error("Insufficient app execution limit to increase child org limit")
return
}
@@ -2939,7 +3038,10 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
const org = subOrgs[orgIndex]
org.sync_features[editing].limit = limit
if (editing !== "app_executions_hard_limit") {
org.sync_features[editing].limit = limit
}
org.sync_features.editing = true
const sync_features = org.sync_features
const data = {
@@ -2947,6 +3049,13 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
sync_features: sync_features,
}
if (editing === "app_executions_hard_limit") {
data.editing = "app_runs_hard_limit";
data.billing = {
app_runs_hard_limit: limit || 0
};
}
const url = `${globalUrl}/api/v1/orgs/${orgId}`;
fetch(url, {
method: "POST",
@@ -2974,6 +3083,12 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
newRows[orgIndex].workflow_usage_limit = limit;
return newRows;
});
}else if (editing === "app_executions_hard_limit") {
setSubOrgStatsRows((prevRows) => {
const newRows = [...prevRows];
newRows[orgIndex].app_runs_hard_limit = limit;
return newRows;
});
}
}
})
@@ -3087,10 +3202,21 @@ const IncreaseLimitPopUp = memo(({ open, onClose, limit, setLimit, HandleEditLim
}
}}
>
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
{editing === "app_executions_hard_limit" ? (
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
Add {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())}
</DialogTitle>
) : (
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit
</DialogTitle>
)}
<DialogContent>
{ editing === "app_executions_hard_limit" ? (
<Typography style={{ marginRight: 20, marginBottom: 20, fontSize: 16, color: theme.palette.text.secondary }}>
Please note that once you set a hard limit for app runs workflows will not be able to run if the limit is reached. You will be notified by email when you reach the limit.
</Typography>
) : null}
<TextField
value={currentLimit}
onChange={(e) => setCurrentLimit(e.target.value)}
+115 -188
View File
@@ -31,57 +31,10 @@ import {
Box,
} from "@mui/material";
import {
BarChart,
BarSeries,
Bar,
BarLabel,
GridlineSeries,
Gridline,
TooltipArea,
ChartTooltip,
TooltipTemplate,
} from 'reaviz';
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
import LineChartWrapper from '../components/LineChartWrapper.jsx';
import { Context } from '../context/ContextApi.jsx';
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data
const {themeMode} = useContext(Context)
const theme = getTheme(themeMode)
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
<Typography variant="h6" style={{marginBotton: 30, }}>
{inputname}
</Typography>
<BarChart
style={{marginTop: 100, }}
width={"100%"}
height={height}
data={inputdata}
series={
<BarSeries
bar={
<Bar />
}
/>
}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
</div>
)
}
const AppStats = (defaultprops) => {
const {
@@ -92,6 +45,12 @@ const AppStats = (defaultprops) => {
inputWorkflows,
clickedFromOrgTab,
syncStats,
statistics,
monthlyAppRunsParent,
setMonthlyAppRunsParent,
monthlyAllSuborgExecutions,
setMonthlyAllSuborgExecutions,
currentTab
} = defaultprops;
const [keys, setKeys] = useState([])
@@ -104,7 +63,6 @@ const AppStats = (defaultprops) => {
const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("")
const [statistics, setStatistics] = useState(undefined);
const [filteredStatistics, setFilteredStatistics] = useState(undefined);
const [apprunCost, setApprunCost] = useState(0)
@@ -125,6 +83,11 @@ const AppStats = (defaultprops) => {
}
}, [])
useEffect(() => {
if (statistics && statistics?.org_id?.length > 0) {
handleDataSetting(statistics, "day")
}
}, [statistics])
const getWorkflowStats = async (workflow, startTime, endTime) => {
@@ -266,6 +229,7 @@ const AppStats = (defaultprops) => {
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) {
setFilteredStatistics(statistics)
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
return
}
@@ -312,20 +276,27 @@ const AppStats = (defaultprops) => {
}
}
// Make a date at the 1st of the current month
// Make a date at the 1st of the current month - only when no start time is selected
var foundstarttime = (new Date())
foundstarttime.setDate(1)
if (startTime !== "" && startTime !== undefined && startTime !== null) {
foundstarttime = startTime
foundstarttime = new Date(startTime)
// Set to start of day to include the entire start date
foundstarttime.setHours(0, 0, 0, 0)
} else {
// Default to 1st of current month when no start time is selected
foundstarttime.setDate(1)
foundstarttime.setHours(0, 0, 0, 0)
}
// Set to tomorrow by default
// Set end time properly
var foundendtime = (new Date())
foundendtime.setDate(foundendtime.getDate() + 1)
// Check if endtime is after the daily statistics["date"] string
if (endTime !== "" && endTime !== undefined && endTime !== null) {
foundendtime = endTime
foundendtime = new Date(endTime)
// Set to end of day to include the entire end date
foundendtime.setHours(23, 59, 59, 999)
} else {
// Default to current date when no end time is selected
foundendtime.setHours(23, 59, 59, 999)
}
// Check if start time is before the daily statistics["date"] string
@@ -337,8 +308,20 @@ const AppStats = (defaultprops) => {
}
const date = new Date(item["date"])
if (date >= foundstarttime) {
if (date <= foundendtime) {
// Normalize the date to start of day for comparison
const normalizedDate = new Date(date)
normalizedDate.setHours(0, 0, 0, 0)
// Normalize foundstarttime for comparison
const normalizedStartTime = new Date(foundstarttime)
normalizedStartTime.setHours(0, 0, 0, 0)
// Normalize foundendtime for comparison
const normalizedEndTime = new Date(foundendtime)
normalizedEndTime.setHours(0, 0, 0, 0)
if (normalizedDate >= normalizedStartTime) {
if (normalizedDate <= normalizedEndTime) {
newlist.push(item)
}
}
@@ -373,6 +356,10 @@ const AppStats = (defaultprops) => {
workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"]
if (currentTab === 0) {
appexecutions += (item["child_app_executions"] ?? 0)
}
estimatedcost += (item["app_executions"] * invocationCost)
}
@@ -390,7 +377,16 @@ const AppStats = (defaultprops) => {
}
setFilteredStatistics(tmpstats)
handleDataSetting(tmpstats, "day")
handleDataSetting(tmpstats, "day")
// if we have done monthly reset than only show monthly app runs as current month app run
const currentMonth = new Date().getMonth() + 1
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
}
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
}
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
@@ -476,7 +472,7 @@ const AppStats = (defaultprops) => {
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]),
data: inputdata["child_app_executions"]
data: item["child_app_executions"]
})
}
@@ -496,40 +492,45 @@ const AppStats = (defaultprops) => {
}
}
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= today.setHours(0, 0, 0, 0))
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
//setApprunCosts(appcostRuns)
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
@@ -541,49 +542,8 @@ const AppStats = (defaultprops) => {
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgid}/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
}
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
if(selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id)
}
}, [selectedOrganization])
const paperStyle = {
textAlign: "center",
padding: "40px",
@@ -703,15 +663,19 @@ const AppStats = (defaultprops) => {
]
const data = (
<div className="content" style={{width: "100%", margin: "auto", }}>
<div className="content" style={{width: "100%", margin: "auto", marginTop: 20}}>
<Typography style={{margin: "auto", marginLeft: 10, marginBottom: 20, fontSize: 16}} color="textSecondary">
All shown statistics are gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/stats`}
target="_blank"
style={{ textDecoration: "none", color: theme.palette.linkColor,}}
>Your Organisation Statistics. </a>
It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. <b>The billing tracker is in Beta, and is always calculated manually before being invoiced.</b>
{currentTab === 0 ?
<span>
All Organization app runs are calculated base on addition of parent org app runs + all child org app runs.
</span>: <span>It exists to give you more insight into your workflows, and to
understand your utilization of the Shuffle platform.{" "}</span>}
<br style={{}}/>
{syncStats !== true ? null :
"PS: You are currently looking at data from your onprem synced org"}
@@ -722,7 +686,7 @@ const AppStats = (defaultprops) => {
{filteredStatistics !== undefined ?
<div style={{flex: 1, display: "flex", textAlign: "center",}}>
{syncStats == true ? null :
{/* {syncStats == true ? null :
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}.
@@ -742,7 +706,7 @@ const AppStats = (defaultprops) => {
</Typography>
</Box>
</Tooltip>
}
} */}
{syncStats === true ? null :
<Tooltip title={
@@ -761,7 +725,7 @@ const AppStats = (defaultprops) => {
</Tooltip>
}
{syncStats === true ? null :
{syncStats === true || currentTab === 0 ? null :
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
Workflow runs in the selected period
@@ -778,7 +742,7 @@ const AppStats = (defaultprops) => {
</Tooltip>
}
{syncStats === true ? null :
{/* {syncStats === true ? null :
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}.
@@ -793,15 +757,9 @@ const AppStats = (defaultprops) => {
</Typography>
</Box>
</Tooltip>
}
</div>
: null}
</div>
{clickedFromOrgTab? (
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
<div style={{ display: "flex", flexDirection: "row", width: "100%", gap: "10px", justifyContent: 'center', alignItems: 'center', paddingTop: 10 }}>
} */}
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", justifyContent: 'center', alignItems: 'flex-start', paddingTop: 10 }}>
<div
style={{
display: "flex",
@@ -925,64 +883,33 @@ const AppStats = (defaultprops) => {
</div>
</div>
</LocalizationProvider>
):(
<LocalizationProvider dateAdapter={AdapterDayjs} style={{flex: 1, }}>
<div style={{display: "flex", flexDirection: "column", }}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
onChange={handleStartTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</div>
</LocalizationProvider>
)}
: null}
</div>
</div>
{appRuns === undefined ?
null
:
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"App Runs - Current Org"}/>
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"App Runs - Current Org"} border={false}/>
}
{childOrgsAppRuns === undefined ?
{childOrgsAppRuns === undefined || currentTab === 1 ?
null
:
<LineChartWrapper keys={childOrgsAppRuns} height={300} width={"100%"} inputname={"Child Org App Runs"}/>
<LineChartWrapper keys={childOrgsAppRuns} height={300} width={"100%"} inputname={"Child Org App Runs"} border={false} />
}
{workflowRuns === undefined ?
{workflowRuns === undefined || currentTab === 0?
null
:
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"}/>
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"} border={false} />
}
{subflowRuns === undefined ?
{subflowRuns === undefined || currentTab === 0 ?
null
:
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"} border={false} />
}
{/*appRunCosts === undefined ?
@@ -991,7 +918,7 @@ const AppStats = (defaultprops) => {
<LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/>
*/}
{syncStats === true ? null :
{syncStats === true || currentTab === 0 ? null :
<div style={{height: 150+resultRows.length * 25, padding: "10px 0px 10px 0px", }}>
{resultLoading ?
<div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}>
@@ -1047,7 +974,7 @@ const AppStats = (defaultprops) => {
)
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
<div style={{ maxWidth: 1366, margin: "auto", }}>{data}</div>
);
return dataWrapper;
+344 -80
View File
@@ -4,7 +4,8 @@ import { getTheme } from "../theme.jsx";
import { toast } from 'react-toastify';
import ReactJson from "react-json-view-ssr";
import { GetIconInfo } from "../views/Workflows2.jsx";
import { GetIconInfo, } from "../views/Workflows2.jsx";
import { validateJson, handleReactJsonClipboard, } from "../views/Workflows.jsx";
import { red } from "../views/AngularWorkflow.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import {
@@ -34,6 +35,7 @@ import {
InputLabel,
Pagination,
PaginationItem,
Avatar,
} from "@mui/material";
import {
@@ -76,8 +78,8 @@ import {
SmartToy as SmartToyIcon,
Settings as SettingsIcon,
FilterAlt as FilterAltIcon,
CompareArrows as CompareArrowsIcon,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
const scrollStyle1 = {
@@ -130,21 +132,22 @@ const CacheView = memo((props) => {
})
const [_, setUpdate] = useState(Math.random())
const [selectedRows, setSelectedRows] = useState([]);
// Direct category migration from ../components/Files.jsx
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
const [renderTextBox, setRenderTextBox] = React.useState(false);
const [datastoreCategories, setDatastoreCategories] = React.useState(["default"]);
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
const [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
const [workflows, setWorkflows] = useState([]);
const [apps, setApps] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const [showAutomationMenu, setShowAutomationMenu] = useState(false);
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
var to_be_copied = "";
const defaultAutomation = [
{
"name": "Run workflow",
@@ -156,6 +159,42 @@ const CacheView = memo((props) => {
"icon": <AirIcon />,
"enabled": false,
},
{
"name": "Correlate Categories",
"description": "",
"type": "singul",
"options": [{
"key": "datastore_categories",
"value": "",
}],
"icon": <CompareArrowsIcon />,
"enabled": false,
"disabled": false,
},
{
"name": "Run AI Agent",
"description": "",
"options": [{
"key": "",
"value": "",
}],
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
{
"name": "Send webhook",
"description": "Sends the updated value to a specified webhook URL.",
"options": [{
"key": "webhook_url",
"value": "",
}],
"icon": <WebhookIcon />,
"enabled": false,
},
{
"name": "Send message",
"description": "",
@@ -180,27 +219,6 @@ const CacheView = memo((props) => {
"enabled": false,
"disabled": true,
},
{
"name": "Run AI Agent",
"description": "",
"options": [{
"key": "",
"value": "",
}],
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
{
"name": "Send webhook",
"description": "Sends the updated value to a specified webhook URL.",
"options": [{
"key": "webhook_url",
"value": "",
}],
"icon": <WebhookIcon />,
"enabled": false,
},
]
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
@@ -210,6 +228,35 @@ const CacheView = memo((props) => {
const theme = getTheme(themeMode, brandColor);
const classes = useStyles();
const getApps = () => {
const url = `${globalUrl}/api/v1/apps`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success === false) {
toast.warn("Failed to load apps. Please try again or contact support@shuffler if this persists.")
} else {
setApps(responseJson)
}
})
.catch((error) => {
toast(error.toString());
});
}
const getWorkflows = () => {
const url = `${globalUrl}/api/v1/workflows`
@@ -251,6 +298,7 @@ const CacheView = memo((props) => {
useEffect(() => {
getWorkflows()
getApps()
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
}, [])
@@ -275,7 +323,6 @@ const CacheView = memo((props) => {
}
var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache`
if (category !== undefined && category !== null && category !== "default" && category !== "") {
url += "?category=" + category.replaceAll(" ", "_")
} else {
@@ -322,7 +369,7 @@ const CacheView = memo((props) => {
.then((responseJson) => {
setCachedLoaded(true);
if (responseJson?.success === true) {
setListCache(responseJson.keys)
setListCache(responseJson.keys);
if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) {
setTotalAmount(responseJson.total_amount)
@@ -351,8 +398,8 @@ const CacheView = memo((props) => {
}
}
if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 1 && datastoreCategories[0] === "default") {
var newcategories = ["default"]
if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 2 && datastoreCategories[0] === "default") {
var newcategories = ["default", "protected"]
for (var key in responseJson.keys) {
var foundcategory = responseJson.keys[key].category
if (foundcategory !== undefined && foundcategory !== null && foundcategory !== ""){
@@ -538,7 +585,7 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New key added Successfully!");
toast.success("New key added!");
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setModalOpen(false);
})
@@ -563,34 +610,6 @@ const CacheView = memo((props) => {
}
}
const handleReactJsonClipboard = (copy) => {
const elementName = "copy_element_shuffle";
let copyText = document.getElementById(elementName);
if (copyText) {
if (copy.namespace && copy.name && copy.src) {
copy = copy.src;
}
const clipboard = navigator.clipboard;
if (!clipboard) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
let stringified = JSON.stringify(copy);
if (stringified.startsWith('"') && stringified.endsWith('"')) {
stringified = stringified.slice(1, -1);
}
navigator.clipboard.writeText(stringified);
toast("Copied value to clipboard, NOT json path.");
} else {
console.log("Failed to copy from " + elementName + ": ", copyText);
}
};
const timestamp = (timestamp) => {
if (timestamp === undefined || timestamp === null || timestamp === "") {
return null
@@ -680,6 +699,7 @@ const CacheView = memo((props) => {
</IconButton>
</Tooltip>
</div>
<TextField
color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }}
@@ -1096,7 +1116,153 @@ const CacheView = memo((props) => {
{showOptions && (
updatedAutomation.options.map((option, optionIndex) => {
if (option?.key === "workflow_id") {
if (option?.key === "datastore_categories") {
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) {
return (
<Typography key={optionIndex} style={{ color: theme.palette.text.secondary, marginTop: 10 }}>
No categories available. Please add categories in the settings.
</Typography>
)
}
return (
<Autocomplete
key={optionIndex}
multiple
label="Choose Datastore Categories"
id="datastore_category_search"
autoHighlight
freeSolo
value={datastoreCategories?.filter(c => option?.value.includes(c)) || []}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: theme.palette.text.primary,
borderRadius: theme.palette.borderRadius,
},
}}
onChange={(event, newValue) => {
console.log("New Value: ", newValue)
option.value = ""
for (var i = 0; i < newValue.length; i++) {
option.value += newValue[i] + ","
}
if (newValue.length > 0) {
updatedAutomation.enabled = true
} else {
updatedAutomation.enabled = false
}
updatedAutomation.options[optionIndex] = option
setUpdatedAutomation(updatedAutomation)
setUpdated(true)
setUpdate(Math.random()) // Force re-render
}}
getOptionLabel={(option) => {
if (option === undefined || option === null) {
return "No Categories Selected";
}
return option
}}
options={datastoreCategories}
fullWidth
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
borderRadius: theme.palette.textFieldStyle.borderRadius,
color: theme.palette.textFieldStyle.color,
height: 35,
marginBottom: 40,
}}
renderOption={(props, data, state) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
const keyfound = option?.value.includes(data)
return (
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose {data}
</Typography>
</span>
} >
<MenuItem
{...props}
style={{
backgroundColor: theme.palette.surfaceColor,
}}
value={data}
>
<Typography style={{
display: "flex",
marginTop: 5,
color: keyfound ? red : theme.palette.text.primary,
}}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color,
borderRadius: theme.palette.textFieldStyle.borderRadius,
height: 35,
fontSize: 16,
marginTop: "16px"
}}
InputProps={{
...params.InputProps,
style: {
height: 35,
display: "flex",
alignItems: "center",
padding: "0px 8px",
fontSize: 16,
borderRadius: 4,
},
inputProps: {
...params.inputProps,
style: {
height: "100%",
boxSizing: "border-box",
}
}
}}
variant="outlined"
placeholder="Select Categories to Correlate"
/>
)
}}
/>
)
} else if (option?.key === "workflow_id") {
return (
<Autocomplete
key={optionIndex}
@@ -1181,8 +1347,7 @@ const CacheView = memo((props) => {
{...props}
style={{
backgroundColor: theme.palette.surfaceColor,
color: data.id === option?.value ? "red" : theme.palette.text.primary,
borderBottom: data.id === "parent" ? "2px solid rgba(255,255,255,0.5)" : null
color: data.id === option?.value ? red : theme.palette.text.primary,
}}
value={data}
>
@@ -1297,12 +1462,25 @@ const CacheView = memo((props) => {
sortable: true,
},
{
width: 600,
width: 540,
field: 'value',
filterable: true,
headerName: 'Value',
renderCell: (props) => {
const data = props.row
if (data?.category?.toLowerCase() === "protected") {
return (
<Typography
variant="body2"
type="password"
style={{maxHeight: 200, overflow: "hidden", }}
>
***************
</Typography>
)
}
const validate = validateJson(data.value)
return (
@@ -1321,22 +1499,17 @@ const CacheView = memo((props) => {
backgroundColor: theme.palette.platformColor,
border: theme.palette.defaultBorder,
padding: 5,
minWidth: 600,
maxHeight: 600,
minWidth: 500,
maxHeight: 500,
overflowY: "auto",
}}
collapsed={true}
enableClipboard={(copy) => {
// handleReactJsonClipboard(copy);
handleReactJsonClipboard(copy)
}}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
iconStyle={theme.palette.jsonIconStyle}
displayDataTypes={false}
onSelect={(select) => {
// HandleJsonCopy(showResult, select, data.action.label);
console.log("SELECTED!: ", select);
}}
name={null}
/>
:
@@ -1348,6 +1521,74 @@ const CacheView = memo((props) => {
)
}
},
{
field: 'category',
headerName: 'Category',
description: 'Category for this key.',
width: 75,
filterable: false,
sortable: true,
renderCell: (props) => {
// Return avatar with hover for the category
const data = props.row
const clickCategory = (e) => {
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(data.category)
return
}
setSelectedCategory(data.category)
if (data.category === "all" || data.category === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, data.category, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data.category}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data.category}`)
}
}
const iconDetails = GetIconInfo({
"app_name": data.category,
"name": data.category,
})
const avatarLetter = (data.category === "" || data.category === "default" ? " " : data.category.charAt(0).toUpperCase())[0]
return (
<Tooltip title={data.category === "" || data.category === "default" ? "No category" : `Category name: ${data.category}`} placement="left">
<Avatar
onClick={(e) => {
clickCategory(e)
}}
style={{
color: "white",
backgroundColor: iconDetails?.iconBackgroundColor || theme.palette.primary.secondary,
marginLeft: 15,
height: 30,
width: 30,
cursor: data.category !== "" && data.category !== "default" ? "pointer" : "default",
}}
variant="rounded"
>
{iconDetails?.originalIcon ?
iconDetails?.originalIcon
:
avatarLetter
}
</Avatar>
</Tooltip>
)
}
},
{
field: 'actions',
headerName: 'Actions',
@@ -1361,7 +1602,7 @@ const CacheView = memo((props) => {
return (
<span style={{ display: "flex" }}>
{data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined ?
{data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined || data?.workflow_id?.length !== 36 ?
<IconButton
disabled={data.workflow_id?.length === 0}
style={{}}
@@ -1369,7 +1610,7 @@ const CacheView = memo((props) => {
<OpenInNewIcon
style={{
color:
data.workflow_id?.length !== 0
data.workflow_id?.length === 36
? "#FF8444"
: "grey",
}}
@@ -1380,6 +1621,7 @@ const CacheView = memo((props) => {
title={"Go to workflow"}
style={{}}
aria-label={"Download"}
placement="left"
>
<span>
<a
@@ -1393,13 +1635,13 @@ const CacheView = memo((props) => {
>
<IconButton
disabled={data.workflow_id?.length ===0}
style={{marginLeft: 10}}
style={{marginLeft: 0}}
>
<OpenInNewIcon
style={{
width: 24, height: 24,
color:
data.workflow_id?.length !== 0
data.workflow_id?.length === 36
? "#FF8444"
: "grey",
}}
@@ -1419,7 +1661,9 @@ const CacheView = memo((props) => {
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
// Try to make the value JSON indented
const valid = validateJson(data.value)
var newvalue = data.value
@@ -1469,7 +1713,9 @@ const CacheView = memo((props) => {
<IconButton
style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
}}
>
@@ -1486,7 +1732,9 @@ const CacheView = memo((props) => {
<IconButton
style={{ padding: "6px" }}
disabled={selectedOrganization?.id === undefined ? false : data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
deleteEntry(orgId, data.key, data.category)
}}
>
@@ -1609,6 +1857,8 @@ const CacheView = memo((props) => {
workflows={workflows}
getWorkflows={getWorkflows}
apps={apps}
/>
{cacheDistributionModal}
@@ -1658,6 +1908,13 @@ const CacheView = memo((props) => {
>
Learn more
</a>
{selectedCategory === "protected" ?
<div style={{ color: red, }}>
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof.
</div>
: null}
</Typography>
</div>
@@ -2067,15 +2324,17 @@ const CacheView = memo((props) => {
<DataGrid
rows={listCache}
columns={columns}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={selectedRows}
onSelectionModelChange={(newSelection) => {
setSelectedRows(newSelection);
}}
onRowSelectionModelChange={(newSelection) => {
setSelectedRows(newSelection)
setSelectedRows(newSelection);
}}
keepNonExistentRowsSelected={false}
getRowId={(row) => row.key}
getRowId={(row) => `${row?.key}_${row?.category}`}
autoHeight={true}
sx={{
@@ -2245,6 +2504,11 @@ const CacheView = memo((props) => {
</div>
</div>
<TextField
id="copy_element_shuffle"
value={to_be_copied}
style={{ display: "none" }}
/>
</div>
);
})
+230 -32
View File
@@ -10,19 +10,27 @@ import {
DialogTitle,
DialogContent,
Typography,
IconButton,
Paper,
LinearProgress,
Grid,
Button,
Tooltip,
Autocomplete,
TextField,
} from '@mui/material';
import {
Rocket as RocketIcon,
FilterAlt as FilterAltIcon,
Add as AddIcon,
} from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CollectIngestModal = (props) => {
const { globalUrl, open, setOpen } = props;
const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props;
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
@@ -37,12 +45,17 @@ const CollectIngestModal = (props) => {
return null
}
const startIngestion = (appname, index) => {
console.log("APPNAME:", appname, "INDEX:", index)
const startIngestion = (bundleName, appnames, category, index) => {
const body = {
"app_name": appname,
"label": appname,
var body = {
"label": bundleName,
"app_name": appnames,
"category": "",
}
if (category !== undefined && category !== null) {
body.category = category
}
const url = `${globalUrl}/api/v2/workflows/generate`
@@ -61,21 +74,29 @@ const CollectIngestModal = (props) => {
return response.json();
})
.then((data) => {
if (getWorkflows !== undefined) {
getWorkflows()
}
console.log("Ingestion started successfully:", data);
toast.success(`Ingestion for ${appname} started successfully!`);
toast.success(`Ingestion for ${bundleName} started successfully!`);
})
.catch((error) => {
console.error("Error starting ingestion:", error);
toast.error(`Failed to start ingestion for ${appname}. Please try again.`);
toast.error(`Failed to start ingestion for ${bundleName}. Please try again or contact support@shuffler.io if this persists.`);
});
}
const IngestItem = (props) => {
const { type, index } = props
const { type, appCategory, index, webhook } = props
const [hovering, setHovering] = useState(false);
const [isFinished, setIsFinished] = useState(false);
const [selectedApps, setSelectedApps] = useState([]);
const [showAppsearch, setShowAppsearch] = useState(false);
const [algoliaOptions, setAlgoliaOptions] = useState([]);
const appname = type
const ingestedAmount = 20
@@ -85,49 +106,222 @@ const CollectIngestModal = (props) => {
"name": appname,
})
var foundMatchingWorkflow = null
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
const parsedName = type.toLowerCase().replaceAll(" ", "_");
const foundWorkflow = workflows.find((workflow) => {
return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName
})
if (foundWorkflow !== undefined && foundWorkflow !== null) {
foundMatchingWorkflow = foundWorkflow
// Find relevant apps and maps them
if (apps.length > 0 && selectedApps.length === 0 && foundWorkflow?.actions !== undefined && foundWorkflow?.actions !== null && foundWorkflow?.actions.length > 0) {
var newSelectedApps = []
for (var actionkey in foundWorkflow.actions) {
const action = foundWorkflow.actions[actionkey]
if (action?.app_name !== "Singul" && action?.app_id !== "integration") {
continue
}
for (var paramkey in action.parameters) {
const param = action.parameters[paramkey]
if (!((param.name === "app_name" || param.name === "appName") && param.value !== undefined && param.value !== null && param.value.length > 0)) {
continue
}
// Find the app in available apps
const parsedname = param.value.replaceAll(" ", "_").toLowerCase()
for (var appkey in apps) {
const appname = apps[appkey].name.replaceAll(" ", "_").toLowerCase()
if (appname === parsedname) {
newSelectedApps.push(apps[appkey])
break
}
}
break
}
}
if (newSelectedApps.length > 0) {
setSelectedApps(newSelectedApps)
}
}
}
}
var matchingapps = []
if (appCategory !== undefined && appCategory !== null && appCategory.length > 0 && apps !== undefined && apps !== null && apps.length > 0) {
for (var appkey in apps) {
const app = apps[appkey]
for (var categorykey in app.categories) {
const category = app.categories[categorykey]
if (category.toLowerCase() === appCategory.toLowerCase()) {
matchingapps.push(app)
}
}
}
}
return (
//<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12}
style={{
minHeight: hovering ? 225 : 145,
maxHeight: hovering ? 225 : 145,
minHeight: hovering ? 200 : 200,
maxHeight: hovering ? "auto" : 140,
cursor: "pointer",
position: "relative",
transition: "all 0.3s ease-in-out",
borderRadius: theme.palette.borderRadius,
border: hovering ? `2px solid ${theme.palette.primary.main}` : isFinished ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`,
border: hovering ? `2px solid ${theme.palette.primary.main}` : foundMatchingWorkflow !== null ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`,
textAlign: "center",
marginBottom: 5,
marginBottom: 10,
overflow: "hidden",
}}
onMouseEnter={() => setHovering(true)}
onMouseEnter={() => {
//if (foundMatchingWorkflow !== null) {
//} else {
setHovering(true)
//}
}}
onMouseLeave={() => setHovering(false)}
>
<div style={{marginTop: 35, marginBottom: 35, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<div style={{display: "flex", }}>
<Typography variant="h4" style={{marginTop: 10, }}>
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
{appname}
</Typography>
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
{selectedApps.map((app, index) => {
// Show image of each one
return (
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
<Tooltip title={app.name} placement="top">
<img
style={{height: 40, width: 40, borderRadius: 50}}
src={app?.large_image || app?.icon || app?.image || "/static/images/default_app_icon.png"}
/>
</Tooltip>
</div>
)
})}
<Tooltip title="Select Apps" placement="top">
<IconButton
style={{marginLeft: 10, marginRight: 50, }}
variant="outlined"
color="secondary"
onClick={() => {
setShowAppsearch(!showAppsearch)
}}
>
<AddIcon style={{color: theme.palette.primary.main, }} />
</IconButton>
</Tooltip>
</div>
{showAppsearch ?
<Autocomplete
style={{flex: 1, maxWidth: 200, minWidth: 200, margin: "auto", marginTop: 10, }}
multiple
filterSelectedOptions
options={matchingapps}
value={selectedApps}
onChange={(event, value) => {
setSelectedApps(value)
}}
getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ")
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
:
<Button
style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}}
>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
}
</div>
<div style={{flex: 1, marginTop: 50, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<Typography variant="h4" style={{marginTop: 10, }}>
{appname}
</Typography>
</div>
</div>
<Button variant="contained" onClick={() => {
toast.info("Starting ingest for relevant apps")
startIngestion(appname, index)
}}>
Start Ingestion
</Button>
{foundMatchingWorkflow !== null ?
<a href={`/workflows/${foundMatchingWorkflow.id}`} target="_blank" rel="noopener noreferrer">
<Tooltip title="View Workflow" placement="right">
<IconButton style={{position: "absolute", top: 10, right: 10, marginLeft: 15, }}>
<RocketIcon style={{ }} />
</IconButton>
</Tooltip>
</a>
: null}
{hovering ?
<div>
</div>
: null}
{isFinished ?
{foundMatchingWorkflow !== null ?
<div>
<Typography variant="body1" style={{
position: "absolute",
@@ -137,6 +331,8 @@ const CollectIngestModal = (props) => {
}}>
{ingestedAmount} / X
</Typography>
{/*
<LinearProgress
style={{
width: "100%",
@@ -146,6 +342,7 @@ const CollectIngestModal = (props) => {
variant="determinate"
fullWidth value={{ingestedAmount}}
/>
*/}
</div>
: null}
</Grid>
@@ -158,7 +355,7 @@ const CollectIngestModal = (props) => {
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: 500,
minWidth: 850,
minHeight: 700,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
@@ -189,9 +386,10 @@ const CollectIngestModal = (props) => {
</Typography>
<Grid container>
<IngestItem type="Ingest Tickets" index={1} />
<IngestItem type="Ingest Tickets" appCategory={"cases"} webhook={true} index={1} />
<IngestItem type="Enable Threat feeds" index={2} />
<IngestItem type="Track Assets" index={2} />
<IngestItem type="Ingest Assets" appCategory={"assets"} index={2} />
<IngestItem type="Ingest Users " appCategory={"users"} index={2} />
<IngestItem type="Enable Search" index={2} />
<IngestItem type="Enable Mitre Att&ck techniques" index={2} />
<IngestItem type="Enable Detection Rules" index={2} />
+7 -77
View File
@@ -1,7 +1,8 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { toast } from "react-toastify";
import LineChartWrapper from '../components/LineChartWrapper.jsx';
export const LoadStats = (globalUrl, cachekey) => {
if (globalUrl === undefined) {
console.log("Error: Global URL is undefined")
@@ -64,84 +65,13 @@ export const LoadStats = (globalUrl, cachekey) => {
})
}
// This wrapper is a waste lol
const DashboardBarchart = (props) => {
// this is clearly unfinished and not worth my time.
// refer to health page to see how i made it work there w/o using chartjs
const { timelineData, title, height, } = props;
// const { timelineData, title, height, } = props;
// var inputHeight = 15
// if (height !== undefined && height !== null) {
// inputHeight = height
// }
// const barOptions = {
// plugins: {
// tooltip: {
// enabled: true, // Ensure tooltips are enabled
// },
// },
// tooltips: {
// mode: 'index',
// intersect: false,
// },
// legend: {
// display: false
// },
// layout: {
// padding: {
// top: 0, // Adjust the top padding as needed
// bottom: -10, // Adjust the bottom padding as needed
// left: 0, // Adjust the left padding as needed
// right: 0, // Adjust the right padding as needed
// },
// },
// scales: {
// y: {
// beginAtZero: false,
// },
// yAxes: [{
// ticks: {
// display: false
// },
// beginAtZero: false,
// }],
// xAxes: [{
// ticks: {
// display: false
// },
// beginAtZero: false,
// }]
// },
// tooltips: {
// callbacks: {
// label: function (tooltipItem, data) {
// const label = data.labels[tooltipItem.index]
// return label.split('\n')[0]
// },
// afterLabel: function (tooltipItem, data) {
// const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value
// return `Amount: ${amount}`
// },
// title: function () {
// return title === undefined ? '' : title
// }
// }
// }
// }
// return (
// <Bar
// data={timelineData}
// options={barOptions}
// height={inputHeight}
// getElementAtEvent={(elements) => {
// if (elements && elements.length > 0) {
// //toast("Click event")
// console.log("Clicked: ", elements)
// }
// }}
// />
// )
return (
<LineChartWrapper keys={timelineData} height={150} width={"100%"} border={false} />
)
}
export default DashboardBarchart;
+2 -37
View File
@@ -25,6 +25,7 @@ const EditOrgTab = (props) => {
handleGetOrg,
selectedStatus, setSelectedStatus,
handleEditOrg,
handleStatusChange
} = props;
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
const [users, setUsers] = React.useState([]);
@@ -39,43 +40,6 @@ const EditOrgTab = (props) => {
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
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", {
@@ -443,6 +407,7 @@ If you're interested, please let me know a time that works for you, or set up a
isEditOrgTab={true}
handleGetOrg={handleGetOrg}
serverside={serverside}
handleStatusChange={handleStatusChange}
/>
</div>
</div >
+535 -78
View File
@@ -3,6 +3,7 @@ import { getTheme } from '../theme.jsx';
import { isMobile } from "react-device-detect"
import { MuiChipsInput } from "mui-chips-input";
import { toast } from "react-toastify"
import ReactGA from 'react-ga4';
import UsecaseSearch from "../components/UsecaseSearch.jsx"
import WorkflowGrid from "../components/WorkflowGrid.jsx"
import dayjs from 'dayjs';
@@ -63,6 +64,10 @@ import {
Add as AddIcon,
Remove as RemoveIcon,
EditNote as EditNoteIcon,
AutoAwesome as AutoAwesomeIcon,
CloudUpload as CloudUploadIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon
} from "@mui/icons-material";
const EditWorkflow = (props) => {
@@ -72,6 +77,7 @@ const EditWorkflow = (props) => {
const {themeMode, brandColor} = useContext(Context)
const theme = getTheme(themeMode, brandColor)
const [submitLoading, setSubmitLoading] = React.useState(false);
const [aiGenerateLoading, setAiGenerateLoading] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false);
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
@@ -92,6 +98,11 @@ const EditWorkflow = (props) => {
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)
// Flowchart upload states
const [uploadedImage, setUploadedImage] = React.useState(null)
const [imageBase64, setImageBase64] = React.useState("")
const [imageUploading, setImageUploading] = React.useState(false)
const classes = useStyles();
@@ -101,6 +112,59 @@ const EditWorkflow = (props) => {
}
}, [formWidth])
// Handle file upload and base64 conversion
const handleImageUpload = (file) => {
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg']
if (!allowedTypes.includes(file.type)) {
toast.error("Please upload a PNG, JPG, or JPEG image")
return
}
const maxSize = 5 * 1024 * 1024 // 5MB in bytes
if (file.size > maxSize) {
toast.error("Image must be less than 5MB")
return
}
setImageUploading(true)
const reader = new FileReader()
reader.onload = (e) => {
const base64 = e.target.result
setImageBase64(base64)
setUploadedImage({
name: file.name,
size: file.size,
type: file.type
})
setImageUploading(false)
// Disable "Create from scratch" when image is uploaded
if (newWorkflow) {
setWorkflowAsCode(false)
}
}
reader.onerror = () => {
toast.error("Failed to read image file")
setImageUploading(false)
}
reader.readAsDataURL(file)
}
const removeUploadedImage = () => {
setUploadedImage(null)
setImageBase64("")
setImageUploading(false)
}
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) {
setTimeout(() => {
const foundScroll = document.getElementById(scrollTo)
@@ -220,7 +284,7 @@ const EditWorkflow = (props) => {
<div style={{ display: "flex" }}>
<div style={{ flex: 1, color: theme.palette.textColor }}>
<div style={{ display: "flex" }}>
<Typography variant="h4" style={{ flex: 9, marginTop: 25, }}>
<Typography variant="h4" style={{ flex: 9, marginTop: newWorkflow ? 50 : 25, }}>
{newWorkflow ? "New" : "Editing"} Workflow
</Typography>
@@ -298,93 +362,371 @@ const EditWorkflow = (props) => {
paddingLeft: 30,
backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor,
}}>
<Button
variant="contained"
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true}
onClick={() => {
setSubmitLoading(true)
{newWorkflow === true ? (
<div style={{ display: "flex", gap: 10 }}>
<Button
variant="contained"
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true || uploadedImage !== null}
onClick={() => {
setSubmitLoading(true)
// Loop inputfields
var validfields = []
for (var i = 0; i < inputQuestions.length; i++) {
if (inputQuestions[i].deleted === true) {
continue
// Loop inputfields
var validfields = []
for (var i = 0; i < inputQuestions.length; i++) {
if (inputQuestions[i].deleted === true) {
continue
}
if (inputQuestions[i].value.length === 0) {
continue
}
validfields.push(inputQuestions[i])
}
innerWorkflow.input_questions = validfields
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) {
innerWorkflow.form_control = {}
}
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) {
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000
}
if (saveWorkflow !== undefined) {
saveWorkflow(innerWorkflow)
if (setWorkflow !== undefined) {
setWorkflow(innerWorkflow)
}
} else if (setNewWorkflow !== undefined) {
setNewWorkflow(
innerWorkflow.name,
innerWorkflow.description,
innerWorkflow.tags,
innerWorkflow.default_return_value,
innerWorkflow,
newWorkflow,
innerWorkflow.usecase_ids,
innerWorkflow.blogpost,
innerWorkflow.status,
workflowAsCode
)
setWorkflow({})
} else {
setWorkflow(innerWorkflow)
}
setSubmitLoading(true)
// If new workflow, don't close it
if (isEditing) {
setModalOpen(false)
}
}}
color="primary"
>
{submitLoading ? <CircularProgress color="secondary" /> : "Create from scratch"}
</Button>
<Tooltip placement="top" arrow
title={
<Typography variant="body1" style={{padding: 10, }}>
Generate using the name, description, usecases and tags provided. Required: Name + (Description OR Flowchart Image)
</Typography>
}
>
<span>
<Button
id="ai-generate-button"
style={{marginLeft: 10, }}
disabled={
name.length === 0 ||
aiGenerateLoading === true ||
submitLoading === true ||
(uploadedImage === null && innerWorkflow?.default_return_value?.trim()?.length === 0)
}
variant="aiButton"
onClick={async () => {
// Track AI Generate button click
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "button_click",
label: userdata?.active_org?.id || "",
});
}
// check AI enabled for local installations (not cloud)
if (!isCloud && (!userdata?.ai_enabled || userdata?.ai_enabled === false)) {
// Toast with onclick
toast.info("Local AI is not enabled, and no cloud AI credits added. Click here to set it up!", {
autoClose: 10000,
onClick: () => {
window.open("/docs/AI#self-hosting-models", "_blank")
}
})
return
}
// Check if description is provided OR image is uploaded for new workflows
if (uploadedImage === null && (!innerWorkflow?.default_return_value || innerWorkflow?.default_return_value?.trim()?.length === 0)) {
toast.error("You need to either upload a flowchart image OR describe what you want to generate so the AI can auto generate the entire workflow");
return;
}
setAiGenerateLoading(true);
toast.info("Creating workflow...");
let workflowId = null;
try {
// Step 1: Create basic workflow WITHOUT using setNewWorkflow (to avoid modal closing)
const workflowData = {
name: name.length > 0 ? name : "AI Generated Workflow",
description: innerWorkflow.default_return_value,
tags: newWorkflowTags,
default_return_value: innerWorkflow.default_return_value,
usecase_ids: selectedUsecases,
blogpost: innerWorkflow.blogpost || "",
status: innerWorkflow.status || ""
};
const workflowResponse = await fetch(`${globalUrl}/api/v1/workflows`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(workflowData),
credentials: "include",
});
if (workflowResponse.status !== 200) {
toast.error("Failed to create workflow");
setAiGenerateLoading(false);
return;
}
const workflowJson = await workflowResponse.json();
if (workflowJson.success === false) {
toast.error("Failed to create workflow: " + (workflowJson.reason || "Unknown error"));
setAiGenerateLoading(false);
return;
}
if (!workflowJson || !workflowJson.id) {
toast.error("Failed to create workflow - no ID returned");
setAiGenerateLoading(false);
return;
}
workflowId = workflowJson.id;
toast.success("Workflow created! AI is now generating your workflow - please wait a few minutes...");
// Step 3: Generate AI content for the workflow
const data = {
query: innerWorkflow.default_return_value,
workflow_id: workflowId,
};
// Only include image_url if an image was uploaded
if (imageBase64 && imageBase64.length > 0) {
data.image_url = imageBase64;
}
const aiResponse = await fetch(globalUrl + "/api/v2/workflows/generate/llm", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
});
const json = await aiResponse.json();
// Handle AI response and provide feedback
if (aiResponse.status === 422) {
// AI rejection with reason
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "ai_rejected",
label: workflowId,
});
}
toast.warning(`AI: ${json.reason || "Request rejected"}. Opening workflow editor...`);
} else if (aiResponse.status !== 200) {
// Other HTTP errors
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_failed",
label: workflowId,
});
}
toast.warning("Workflow created, but AI generation failed. Opening workflow editor...");
} else {
// Successful generation
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_success",
label: workflowId,
});
}
toast.success("Workflow generated successfully! Opening editor...");
}
// Step 4: Close modal and redirect (after everything is complete)
setTimeout(() => {
setModalOpen(false);
setAiGenerateLoading(false);
window.location.href = `/workflows/${workflowId}`;
}, 1500);
} catch (error) {
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "error",
label: workflowId || "no_workflow",
});
}
toast.error("Failed to generate. Please try again later: " + error.message);
setAiGenerateLoading(false);
}
}}
>
{aiGenerateLoading ? (
<CircularProgress color="secondary" size={20} style={{ marginRight: 8 }} />
) : (
<AutoAwesomeIcon style={{ marginRight: 8 }} />
)}
AI Generate
</Button>
</span>
</Tooltip>
</div>
) : (
<Button
variant="contained"
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true}
onClick={() => {
setSubmitLoading(true)
// Loop inputfields
var validfields = []
for (var i = 0; i < inputQuestions.length; i++) {
if (inputQuestions[i].deleted === true) {
continue
}
if (inputQuestions[i].value.length === 0) {
continue
}
validfields.push(inputQuestions[i])
}
if (inputQuestions[i].value.length === 0) {
continue
innerWorkflow.input_questions = validfields
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) {
innerWorkflow.form_control = {}
}
validfields.push(inputQuestions[i])
}
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.input_questions = validfields
innerWorkflow.name = name
innerWorkflow.description = description
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) {
innerWorkflow.form_control = {}
}
if (newWorkflowTags.length > 0) {
innerWorkflow.tags = newWorkflowTags
} else {
innerWorkflow.tags = []
}
innerWorkflow.form_control.input_markdown = inputMarkdown
innerWorkflow.form_control.output_yields = selectedYieldActions
innerWorkflow.form_control.form_width = formWidth
innerWorkflow.form_control.cleanup_actions = selectedCleanupActions
if (selectedUsecases.length > 0) {
innerWorkflow.usecase_ids = selectedUsecases
} else {
innerWorkflow.usecase_ids = []
}
innerWorkflow.name = name
innerWorkflow.description = description
if (dueDate > 0) {
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000
}
if (newWorkflowTags.length > 0) {
innerWorkflow.tags = newWorkflowTags
} else {
innerWorkflow.tags = []
}
if (saveWorkflow !== undefined) {
saveWorkflow(innerWorkflow)
if (selectedUsecases.length > 0) {
innerWorkflow.usecase_ids = selectedUsecases
} else {
innerWorkflow.usecase_ids = []
}
if (dueDate > 0) {
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000
}
if (saveWorkflow !== undefined) {
saveWorkflow(innerWorkflow)
if (setWorkflow !== undefined) {
if (setWorkflow !== undefined) {
setWorkflow(innerWorkflow)
}
} else if (setNewWorkflow !== undefined) {
setNewWorkflow(
innerWorkflow.name,
innerWorkflow.description,
innerWorkflow.tags,
innerWorkflow.default_return_value,
innerWorkflow,
newWorkflow,
innerWorkflow.usecase_ids,
innerWorkflow.blogpost,
innerWorkflow.status,
workflowAsCode
)
setWorkflow({})
} else {
setWorkflow(innerWorkflow)
}
} else if (setNewWorkflow !== undefined) {
setNewWorkflow(
innerWorkflow.name,
innerWorkflow.description,
innerWorkflow.tags,
innerWorkflow.default_return_value,
innerWorkflow,
newWorkflow,
innerWorkflow.usecase_ids,
innerWorkflow.blogpost,
innerWorkflow.status,
workflowAsCode
)
setWorkflow({})
} else {
setWorkflow(innerWorkflow)
}
setSubmitLoading(true)
setSubmitLoading(true)
// If new workflow, don't close it
if (isEditing) {
setModalOpen(false)
}
}}
color="primary"
>
{submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
</Button>
// If new workflow, don't close it
if (isEditing) {
setModalOpen(false)
}
}}
color="primary"
>
{submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
</Button>
)}
</div>
<DialogContent style={{ paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, paddingBottom: 400, paddingLeft: 50, }}>
@@ -409,6 +751,30 @@ const EditWorkflow = (props) => {
id="Enter-Workflow-Name"
/>
{newWorkflow === true ?
<TextField
id="Workflow-Description"
onBlur={(event) => {
innerWorkflow.default_return_value = event.target.value
setInnerWorkflow(innerWorkflow)
setUpdate(Math.random())
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={innerWorkflow.default_return_value}
placeholder="Please describe your workflow below so the AI can generate it."
rows="3"
multiline
label="Description"
margin="dense"
fullWidth
/>
: null}
<div style={{ display: "flex", marginTop: 10, }}>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<FormControl style={{ flex: 1, marginRight: 5, }}>
@@ -505,6 +871,98 @@ const EditWorkflow = (props) => {
/>
</div>
{/* Flowchart Upload Section - Only for new workflows */}
{newWorkflow === true ? (
<div style={{ marginTop: 100, }}>
{!uploadedImage ? (
<div
style={{
border: `1px solid ${theme.palette.primary.main}`,
borderRadius: 8,
padding: 20,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease',
backgroundColor: theme.palette.surfaceColor,
'&:hover': {
backgroundColor: theme.palette.primary.main + '10'
}
}}
onClick={() => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/png,image/jpeg,image/jpg'
input.onchange = (e) => {
if (e.target.files.length > 0) {
handleImageUpload(e.target.files[0])
}
}
input.click()
}}
>
{imageUploading ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
<CircularProgress size={24} style={{ marginBottom: 10 }} />
<Typography variant="body2" color="textSecondary">
Processing image...
</Typography>
</div>
) : (
<div>
<CloudUploadIcon
style={{
fontSize: 48,
color: theme.palette.secondary || '#666',
marginBottom: 10
}}
/>
<Typography variant="h6" style={{ marginBottom: 5 }}>
Generate Workflow from Flowchart
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
Click to upload your flowchart - AI will convert it to a workflow
</Typography>
<Typography variant="caption" color="textSecondary">
PNG, JPG, JPEG Max 5MB
</Typography>
</div>
)}
</div>
) : (
<div
style={{
border: `1px solid ${theme.palette.primary.main}`,
borderRadius: 8,
padding: 15,
backgroundColor: theme.palette.primary.main + '10',
display: 'flex',
alignItems: 'center',
gap: 15
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 10 }}>
<CheckCircleIcon style={{ color: theme.palette.success.main }} />
<div>
<Typography variant="body2" style={{ fontWeight: 'bold' }}>
{uploadedImage.name}
</Typography>
<Typography variant="caption" color="textSecondary">
{formatFileSize(uploadedImage.size)}
</Typography>
</div>
</div>
<IconButton
onClick={removeUploadedImage}
style={{ color: theme.palette.text.secondary }}
size="small"
>
<CloseIcon />
</IconButton>
</div>
)}
</div>
) : null}
{showMoreClicked === true ?
<div style={{ marginTop: 50, }}>
<TextField
@@ -856,7 +1314,7 @@ const EditWorkflow = (props) => {
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
<b>Beta Feature</b>: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
</Typography>
<FormControl style={{ marginTop: 15, }}>
@@ -1285,7 +1743,7 @@ const EditWorkflow = (props) => {
</DialogContent>
{newWorkflow === true ?
{/*newWorkflow === true ?
<span style={{ paddingTop: 30 }}>
<Typography variant="h6" style={{ marginLeft: 30, paddingBottom: 0, }}>
Relevant Workflows
@@ -1311,9 +1769,8 @@ const EditWorkflow = (props) => {
}
</span>
: null}
{/*newWorkflow === true && name.length > 2 ?
: null*/}
{/*newWorkflow === true && name.length > 2 ?
<div style={{marginLeft: 30, }}>
<WorkflowGrid
maxRows={1}
+36 -21
View File
@@ -383,38 +383,36 @@ const EnvironmentTab = memo((props) => {
//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, "\\!")
// 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 \\
@@ -428,14 +426,32 @@ const EnvironmentTab = memo((props) => {
-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 \\` : ""}
-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`
return `helm install shuffle-orborus oci://ghcr.io/shuffle/charts/shuffle \\
--namespace shuffle --create-namespace \\
--set opensearch.enabled=false \\
--set backend.replicaCount=0 \\
--set frontend.replicaCount=0 \\
--set 'orborus.extraEnvVars[0].name=AUTH' \\
--set 'orborus.extraEnvVars[0].value=${auth}' \\
--set 'orborus.extraEnvVars[1].name=BASE_URL' \\
--set 'orborus.extraEnvVars[1].value=${newUrl}' \\
--set 'orborus.extraEnvVars[2].name=ENVIRONMENT_NAME' \\
--set 'orborus.extraEnvVars[2].value=${environment.Name}' \\
--set 'orborus.extraEnvVars[3].name=ORG' \\
--set 'orborus.extraEnvVars[3].value=${environment.org_id}' \\
--set shuffle.org="${environment.org_id}"${addProxy ? ` \\
--set 'orborus.extraEnvVars[4].name=HTTPS_PROXY' \\
--set 'orborus.extraEnvVars[4].value=IP:PORT'` : ""}${skipPipeline ? ` \\
--set 'orborus.extraEnvVars[${addProxy ? 5 : 4}].name=SHUFFLE_SKIP_PIPELINES' \\
--set 'orborus.extraEnvVars[${addProxy ? 5 : 4}].value=true'` : ""} \\
--set persistence.storageClass=standard`
}
const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\
--restart=always \\
--name="shuffle-orborus" \\
@@ -445,12 +461,13 @@ const EnvironmentTab = memo((props) => {
-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 \\` : ""}
-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
@@ -1475,7 +1492,7 @@ const EnvironmentTab = memo((props) => {
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ?
<Typography variant='body2' color="textSecondary">
Check our <a href="https://docs.docker.com/get-started/get-docker/" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Kubernetes documentation</a> for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected.
Simply connect to your Kubernetes cluster and run the following command:
</Typography>
:
<Typography variant='body2' color="textSecondary">
@@ -1489,7 +1506,6 @@ const EnvironmentTab = memo((props) => {
"2. Run this command on the server you want to run workflows or store Pipeline data on"}
</Typography>
{installationTab === 2 ? null :
<div
style={{
marginTop: 10,
@@ -1571,8 +1587,7 @@ const EnvironmentTab = memo((props) => {
}}
/>
</div>
</div>
}
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? null :
File diff suppressed because it is too large Load Diff
+87 -16
View File
@@ -1,22 +1,93 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import React, { useState } from 'react';
const HealthBarChart = (props) => {
const { globalUrl, filteredData, options, onBarClick } = props;
return (
<Bar
data={filteredData}
options={options}
height="35.5rem"
width={filteredData.width}
getElementAtEvent={(elements) => {
if (elements && elements.length > 0) {
onBarClick(elements);
}
const { filteredData, onBarClick } = props;
const [hoveredBar, setHoveredBar] = useState(null);
if (!filteredData || !Array.isArray(filteredData)) {
console.error('Invalid chart data format');
return null;
}
const formatData = () => {
return filteredData.map((item) => ({
label: item.date,
value: item.avgRunFinished,
color: item.color,
executionIds: item.executionIds,
}));
};
const handleBarClick = (data) => {
if (onBarClick) {
onBarClick(data);
}
};
const chartData = formatData();
const barWidth = `calc((100% - ${(chartData.length - 1) * 5}px) / ${chartData.length})`;
const barGap = '5px';
return (
<div style={{
height: '5rem',
width: '100%',
position: 'relative',
display: 'flex',
alignItems: 'center', // Center the content vertically
justifyContent: 'center', // Center the content horizontally
padding: '0.5rem 0' // Add some padding for spacing
}}>
{/* Chart Container */}
<div style={{
display: 'flex',
height: '80%', // Take up 80% of the parent height
width: '100%',
alignItems: 'center', // Center the bars vertically
gap: barGap,
}}>
{chartData.map((item, index) => (
<div
key={index}
style={{
width: barWidth,
height: '70%', // 70% of the container height
backgroundColor: item.color,
cursor: 'pointer',
borderRadius: '2px',
}}
/>
);
onMouseEnter={() => setHoveredBar(item)}
onMouseLeave={() => setHoveredBar(null)}
onClick={() => handleBarClick(item)}
/>
))}
</div>
{/* Tooltip */}
{hoveredBar && (
<div
style={{
position: 'absolute',
top: '-3rem', // Position above the chart
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: '#333333',
color: '#ffffff',
padding: '8px',
borderRadius: '4px',
fontSize: '12px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
zIndex: 1000,
}}
>
<div>Date: {hoveredBar.label}</div>
<div>Uptime: {hoveredBar.value}%</div>
<div>Executions: {hoveredBar.executionIds.length}</div>
</div>
)}
</div>
);
};
export default HealthBarChart;
+226 -60
View File
@@ -1,31 +1,35 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Bar } from 'react-chartjs-2';
//import healthData1 from '../healthstats1.json';
import { toast } from "react-toastify"
import CheckOutlinedIcon from '@mui/icons-material/CheckOutlined';
import { toast } from "react-toastify";
import {
CheckOutlined as CheckOutlinedIcon,
} from '@mui/icons-material';
import {
Button,
ButtonGroup,
Typography,
LinearProgress,
} from "@mui/material";
import HealthBarChart from '../components/HealthBarChart.jsx';
import LiveExecutionsChart from '../components/LiveExecutionsGraph.jsx';
const HealthPage = (props) => {
const { globalUrl, userdata } = props;
const { userdata, globalUrl } = props;
const [healthData, setHealthData] = useState(null);
const [selectedRange, setSelectedRange] = useState('30d');
const [liveExecutionsData, setLiveExecutionsData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [averageUptime, setAverageUptime] = useState(0);
const [liveExecutionsRange, setLiveExecutionsRange] = useState('1h'); // Default to 1h
const [isHealthLoading, setIsHealthLoading] = useState(false); // Loading state for HealthBarChart
const [isLiveExecutionsLoading, setIsLiveExecutionsLoading] = useState(false); // Loading state for LiveExecutionsChart
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
//const globalUrl = `https://shuffler.io`
console.log("HEALTHPAGE 1")
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
const fetchHealthStats = useCallback(async () => {
//const after = new Date().getTime() - 90 * 24 * 60 * 60
setIsHealthLoading(true); // Start loading for HealthBarChart
try {
//const response = await fetch(`${globalUrl}/api/v1/health/stats?after=${after}`, {
const response = await fetch(`${globalUrl}/api/v1/health/stats`, {
method: "GET",
headers: {
@@ -42,31 +46,118 @@ const HealthPage = (props) => {
} catch (error) {
console.error("Error fetching health stats:", error);
toast.error("Failed loading health stats");
} finally {
setIsHealthLoading(false); // Stop loading for HealthBarChart
}
}, [globalUrl]);
const fetchLiveExecutions = useCallback(async (range = '1h') => {
setIsLiveExecutionsLoading(true); // Start loading for LiveExecutionsChart
try {
const fetchOptions = {
method: "GET",
credentials: "include",
};
if (window.location.host !== "localhost:3002") {
fetchOptions.headers = {
"Content-Type": "application/json",
Accept: "application/json",
};
}
const now = Math.floor(Date.now() / 1000);
// let after = now - 3600; // Default to 1h
let mode = ""
switch (range) {
case '1h':
mode = "1h"
break;
case '7h':
mode = "7h"
break;
case '1d':
mode = "1d"
break;
case '7d':
mode = "7d"
break;
case 'month':
mode = "month"
break;
default:
mode = "1h"
}
if (!userdata.support_access) {
return
}
const response = await fetch(
`${globalUrl}/api/v1/health/executions/live?mode=${mode}`,
fetchOptions
);
if (!response.ok) {
throw new Error("Failed to fetch live executions");
}
const data = await response.json();
console.log("Raw live executions data:", data);
if (Array.isArray(data)) {
const formattedData = data
.map(item => ({
...item,
// failed: Number(item.failed) || 0,
executing: Number(item.executing) || 0,
finished: Number(item.finished) || 0,
aborted: Number(item.aborted) || 0,
created_at: Number(item.created_at) || 0
}))
.sort((a, b) => a.created_at - b.created_at);
console.log("Formatted live executions data:", formattedData);
setLiveExecutionsData(formattedData);
} else {
console.error("Received invalid data format:", data);
setLiveExecutionsData([]);
}
} catch (error) {
console.error("Error fetching live executions:", error);
toast.error("Failed loading live executions data");
} finally {
setIsLiveExecutionsLoading(false); // Stop loading for LiveExecutionsChart
}
}, [globalUrl]);
useEffect(() => {
fetchHealthStats();
}, [fetchHealthStats]);
fetchLiveExecutions(liveExecutionsRange);
const interval = setInterval(() => fetchLiveExecutions(liveExecutionsRange), 60000);
return () => clearInterval(interval);
}, [fetchHealthStats, fetchLiveExecutions, liveExecutionsRange]);
const extractRunFinished = (data, range) => {
if (!data || !Array.isArray(data)) return [];
const currentDate = new Date().getTime();
const rangeInMillis = {
'24hr': 24 * 60 * 60 * 1000,
'7day': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
'90d': 90 * 24 * 60 * 60 * 1000
}
const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range])
const aggregatedData = new Map()
};
const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range]);
const aggregatedData = new Map();
filteredData.forEach(item => {
const timestamp = item.updated * 1000; // Convert Unix timestamp to milliseconds
let key;
switch (range) {
case '24hr':
const date = new Date(timestamp);
@@ -83,7 +174,7 @@ const HealthPage = (props) => {
default:
key = new Date(timestamp).toLocaleDateString();
}
// Check if date already exists in the map
if (aggregatedData.has(key)) {
// Update aggregated values
@@ -100,13 +191,13 @@ const HealthPage = (props) => {
});
}
});
// Calculate averages and assign colors
const result = Array.from(aggregatedData.entries()).map(([key, { totalEntries, totalRunFinished, executionIds }]) => {
const avg = totalEntries > 0 ? totalRunFinished / totalEntries : 0;
const FinalAvg = avg * 100;
let color;
if (FinalAvg >= 100) {
color = '#00F670';
} else if (FinalAvg >= 98.50 && FinalAvg <= 99.99) {
@@ -114,7 +205,7 @@ const HealthPage = (props) => {
} else if (FinalAvg <= 98.49) {
color = '#FF354C';
}
return {
date: range === '24hr' ? `${key}:00` : key,
avgRunFinished: FinalAvg,
@@ -122,11 +213,10 @@ const HealthPage = (props) => {
executionIds
};
});
return result;
};
useEffect(() => {
if (healthData) {
const newData = extractRunFinished(healthData, selectedRange);
@@ -160,19 +250,19 @@ const HealthPage = (props) => {
const labels = filteredData.map((value, i) => {
if (selectedRange === '24hr') {
const [datePart, hourPart] = value.date.split(' ');
const [day, month, year] = datePart.split('/');
const [month, day, year] = datePart.split('/');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex, day);
let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`;
// Add hour part if available
if (hourPart) {
formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`;
}
return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`;
} else if (selectedRange === '7day') {
const [datePart, hourPart] = value.date.split(' ');
const [day, month, year] = datePart.split('/');
const [month, day, year] = datePart.split('/');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex, day);
let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`;
@@ -185,12 +275,12 @@ const HealthPage = (props) => {
}
else {
const dateParts = value.date.split('/'); // Assuming the date format is "DD/MM/YYYY"
const date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); // Reformat the date string to "YYYY-MM-DD"
const date = new Date(`${dateParts[2]}-${dateParts[0]}-${dateParts[1]}`); // Reformat the date string to "YYYY-MM-DD"
return `${date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })} \nUptime: ${value.avgRunFinished.toFixed(2)}%`;
}
});
if (selectedRange === '24hr') {
barThickness = 35;
}
@@ -200,12 +290,6 @@ const HealthPage = (props) => {
else if (selectedRange === '30d') {
barThickness = 25;
}
// let width = 800; // Default chart width
// if (selectedRange === '7day') {
// width = 400; // Adjust chart width for 7 days
// } else if (selectedRange === '30d') {
// width = 600; // Adjust chart width for 30 days
// }
const datasets = [{
label: "",
@@ -213,14 +297,11 @@ const HealthPage = (props) => {
backgroundColor: filteredData.map(item => item.color),
borderWidth: 1,
barThickness: barThickness,
// barPercentage: barPercentage,
}];
return { labels, datasets };
};
console.log("HEALTHPAGE 2")
const options = {
legend: {
display: false
@@ -253,12 +334,11 @@ const HealthPage = (props) => {
},
afterLabel: function (tooltipItem, data) {
const label = data.labels[tooltipItem.index];
// console.log(label)
const uptime = label.match(/Uptime:\s*(\d+(?:\.\d+)?)/)[1]; // Extract uptime value using regex
return `Success Rate: ${uptime}%`; // Customize the uptime display
return `Test-Workflow Health: ${uptime}%`; // Customize the uptime display
},
title: function () {
return 'Fully Oprational'; // Hide the tooltip title
return 'Fully Operational'; // Hide the tooltip title
}
}
}
@@ -284,32 +364,118 @@ const HealthPage = (props) => {
}
};
const healthBarData = updateChartData()
return (
<div style={{ padding: 30, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ paddingTop: 30, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
{/* Health Bar Chart Section */}
<ButtonGroup style={{ display: 'flex', margin: "auto", marginBottom: 10, width: 300, borderRadius: 30, background: "#000000" }}>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '24hr' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '24hr' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('24hr')}>24h</Button>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '7day' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '7day' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('7day')}>7d</Button>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '30d' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '30d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('30d')}>30d</Button>
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '90d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '90d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('90d')}>90d</Button>
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '180d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '180d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('180d')}>180d</Button>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '24hr' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '24hr' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('24hr')} disabled={isHealthLoading}>24h</Button>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '7day' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '7day' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('7day')} disabled={isHealthLoading}>7d</Button>
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '30d' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '30d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('30d')} disabled={isHealthLoading}>30d</Button>
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '90d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '90d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('90d')} disabled={isHealthLoading}>90d</Button>
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '180d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '180d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('180d')} disabled={isHealthLoading}>180d</Button>
</ButtonGroup>
<div style={{ margin: '0 auto', padding: 20, width: 1000, justifyContent: "center", color: '#ffffff', backgroundColor: '#000000', fontSize: '16px', borderRadius: '16px' }}>
<div style={{ display: "flex", alignItems: "center" }}>
<CheckOutlinedIcon style={{ borderRadius: 20, fontSize: 24, backgroundColor: '#00e600', marginLeft: 25 }} />
<div>
<Typography style={{ marginLeft: 10 }}>Workflow Health</Typography>
<Typography style={{ marginLeft: 10, fontWeight: 100, fontSize: 13, color: "#00FF00" }}>Operational</Typography>
{/* Loading Bar for HealthBarChart */}
{isHealthLoading && (
<LinearProgress style={{ width: '100%', marginBottom: 10 }} />
)}
<div style={{ margin: '0 auto', padding: 10, width: 800, backgroundColor: '#000000', borderRadius: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<CheckOutlinedIcon style={{ borderRadius: 20, fontSize: 24, backgroundColor: '#00e600', marginRight: 10 }} />
<div>
<Typography style={{ color: '#ffffff', fontSize: 16 }}>Workflow Health</Typography>
<Typography style={{ color: '#00FF00', fontSize: 12, fontWeight: 100 }}>Operational</Typography>
</div>
</div>
<div style={{ marginLeft: 720 }}>
<Typography style={{}}>{averageUptime.toFixed(2)}%</Typography>
<Typography style={{ fontWeight: 100, fontSize: 13, textAlign: "end" }}>Success Rate</Typography>
<div>
<Typography style={{ color: '#ffffff', fontSize: 16, textAlign: 'end' }}>{averageUptime.toFixed(2)}%</Typography>
<Typography style={{ color: '#ffffff', fontSize: 12, fontWeight: 100 }}>Success Rate</Typography>
</div>
</div>
<HealthBarChart filteredData={updateChartData()} options={options} onBarClick={handleBarClick} />
<HealthBarChart
filteredData={filteredData}
onBarClick={handleBarClick}
/>
</div>
{userdata.support_access && (
<div style={{ margin: '20px auto', padding: 20, width: 1000, justifyContent: "center", color: '#ffffff', backgroundColor: '#000000', fontSize: '16px', borderRadius: '16px' }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 20 }}>
<Typography variant="h6" style={{ marginLeft: 10 }}>Live Executions</Typography>
<ButtonGroup style={{ marginLeft: 'auto', borderRadius: 30, background: "#000000" }}>
<Button
variant="contained"
style={{
flex: 1,
borderBottom: liveExecutionsRange === '1h' ? '2px solid #FF8444' : 'none',
background: "#000000",
color: liveExecutionsRange === '1h' ? '#FF8444' : '#cfd8dc',
textTransform: 'none'
}}
onClick={() => setLiveExecutionsRange('1h')}
disabled={isLiveExecutionsLoading}
>
1h
</Button>
<Button
variant="contained"
style={{
flex: 1,
borderBottom: liveExecutionsRange === '7h' ? '2px solid #FF8444' : 'none',
background: "#000000",
color: liveExecutionsRange === '7h' ? '#FF8444' : '#cfd8dc',
textTransform: 'none'
}}
onClick={() => setLiveExecutionsRange('7h')}
disabled={isLiveExecutionsLoading}
>
7h
</Button>
<Button
variant="contained"
style={{
flex: 1,
borderBottom: liveExecutionsRange === '1d' ? '2px solid #FF8444' : 'none',
background: "#000000",
color: liveExecutionsRange === '1d' ? '#FF8444' : '#cfd8dc',
textTransform: 'none'
}}
onClick={() => setLiveExecutionsRange('1d')}
disabled={isLiveExecutionsLoading}
>
1d
</Button>
<Button
variant="contained"
style={{
flex: 1,
borderBottom: liveExecutionsRange === '7d' ? '2px solid #FF8444' : 'none',
background: "#000000",
color: liveExecutionsRange === '7d' ? '#FF8444' : '#cfd8dc',
textTransform: 'none'
}}
onClick={() => setLiveExecutionsRange('7d')}
disabled={isLiveExecutionsLoading}
>
7d
</Button>
{/* <Button variant="contained" style={{ flex: 1, borderBottom: liveExecutionsRange === 'month' ? '2px solid #FF8444' : 'none', background: "#000000", color: liveExecutionsRange === 'month' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => setLiveExecutionsRange('month')} disabled={isLiveExecutionsLoading}>Month</Button> */}
</ButtonGroup>
</div>
{/* Loading Bar for LiveExecutionsChart */}
{isLiveExecutionsLoading && (
<LinearProgress style={{ width: '100%', marginBottom: 10 }} />
)}
<LiveExecutionsChart data={liveExecutionsData} />
</div>
)}
</div>
);
};
@@ -0,0 +1,257 @@
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import ReactJson from 'react-json-view-ssr';
import CodeMirror from '@uiw/react-codemirror';
import { python } from '@codemirror/lang-python';
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
const HighlightedValueInSearch = ({ value, searchTerm, theme }) => {
const containerRef = useRef(null);
const preRef = useRef(null);
const scrollTimeoutRef = useRef(null);
const isJson = useCallback((str) => {
if (typeof str === 'object' && str !== null) return true;
if (typeof str !== 'string') return false;
const trimmed = str.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false;
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
}, []);
const getJsonValue = useCallback((val) => {
try {
return typeof val === 'object' ? val : JSON.parse(val.trim());
} catch {
return null;
}
}, []);
const highlightInJson = useCallback((jsonStr, term) => {
if (!term) return jsonStr;
try {
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedTerm})`, 'gi');
const highlightId = `highlight-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
return jsonStr.replace(regex, `<mark id="${highlightId}" style="background: rgba(255,255,0,0.4); padding: 0 2px; border-radius: 2px;">$1</mark>`);
} catch {
return jsonStr;
}
}, []);
const isPythonCode = useCallback((str) => {
if (typeof str !== 'string') return false;
const pythonPatterns = [
'import ', 'from ', 'def ', 'class ', 'if __name__',
'print(', 'return ', 'elif ', 'except:', 'try:', 'with ',
'lambda ', 'yield ', 'async def', 'await '
];
const trimmed = str.trim();
return pythonPatterns.some(pattern => trimmed.includes(pattern)) &&
(trimmed.includes('def') || trimmed.includes('class'));
}, []);
useEffect(() => {
if (!searchTerm || !containerRef.current || !preRef.current) return;
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
try {
const container = containerRef.current;
const firstHighlight = preRef.current?.querySelector('mark');
if (firstHighlight && container) {
const containerRect = container.getBoundingClientRect();
const highlightRect = firstHighlight.getBoundingClientRect();
const scrollTop = highlightRect.top - containerRect.top + container.scrollTop - (container.clientHeight / 2);
container.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' });
}
} catch (error) {
console.warn('Auto-scroll failed:', error);
}
}, 100);
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [searchTerm, value]);
const RegularText = useCallback(({ value: textValue, searchTerm: term }) => {
if (!textValue) return <span>No content</span>;
const stringValue = String(textValue);
// Check if it's Python code
if (isPythonCode(stringValue)) {
const lines = stringValue.split('\n');
const isNodeView = lines[0]?.startsWith('Node:');
const nodeTitle = isNodeView ? lines[0] : null;
const codeContent = isNodeView ? lines.slice(1).join('\n') : stringValue;
return (
<div style={{
backgroundColor: '#1e1e1e',
borderRadius: '4px',
overflow: 'hidden',
}}>
{nodeTitle && (
<div style={{
padding: '8px 12px',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
color: '#E0E0E0',
fontSize: '14px',
fontWeight: 500,
}}>
{nodeTitle}
</div>
)}
<div style={{
padding: '8px',
maxHeight: '400px',
overflow: 'auto',
}}>
<CodeMirror
value={codeContent}
theme={vscodeDark}
extensions={[python()]}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: false,
highlightActiveLineGutter: false,
highlightSpecialChars: false,
drawSelection: false,
}}
editable={false}
style={{
fontSize: '13px',
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace',
}}
height="auto"
/>
</div>
</div>
);
}
if (!term) {
return (
<span style={{
wordBreak: 'break-word',
overflowWrap: 'break-word',
display: 'inline-block',
maxWidth: '100%'
}}>
{stringValue}
</span>
);
}
try {
const parts = stringValue.split(term);
return (
<span style={{
wordBreak: 'break-word',
overflowWrap: 'break-word',
display: 'inline-block',
maxWidth: '100%'
}}>
{parts.map((part, index) => (
<React.Fragment key={index}>
{part}
{index < parts.length - 1 && (
<span style={{
backgroundColor: 'rgba(255, 255, 0, 0.3)',
padding: '0 2px',
borderRadius: '2px',
}}>
{term}
</span>
)}
</React.Fragment>
))}
</span>
);
} catch {
return <span>{stringValue}</span>;
}
}, [isPythonCode]);
if (value == null) {
return <span style={{ color: '#888', fontStyle: 'italic' }}>null</span>;
}
try {
if (isJson(value)) {
const jsonValue = getJsonValue(value);
if (jsonValue === null) {
return <RegularText value={String(value)} searchTerm={searchTerm} />;
}
const jsonString = JSON.stringify(jsonValue, null, 2);
const hasMatch = searchTerm && jsonString.toLowerCase().includes(searchTerm.toLowerCase());
return (
<div
ref={containerRef}
style={{
backgroundColor: 'rgba(0,0,0,0.2)',
padding: '8px',
borderRadius: '4px',
maxHeight: '200px',
overflow: 'auto'
}}
>
{hasMatch ? (
<pre
ref={preRef}
style={{
fontFamily: 'Monaco, monospace',
fontSize: '12px',
color: '#d4d4d4',
margin: 0,
whiteSpace: 'pre-wrap'
}}
dangerouslySetInnerHTML={{ __html: highlightInJson(jsonString, searchTerm) }}
/>
) : (
<ReactJson
src={jsonValue}
theme={theme?.palette?.jsonTheme || 'monokai'}
name={false}
collapsed={2}
enableClipboard={true}
style={{ backgroundColor: 'transparent', fontSize: '12px' }}
displayDataTypes={false}
/>
)}
</div>
);
}
return <RegularText value={value} searchTerm={searchTerm} />;
} catch (error) {
console.error('HighlightedValueInSearch error:', error);
return <RegularText value={String(value)} searchTerm={searchTerm} />;
}
};
export default React.memo(HighlightedValueInSearch);
+4 -2
View File
@@ -578,7 +578,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
},
}}
>
{userdata && (userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org")) ? null : (
{userdata && (userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.role !== "admin") ? null : (
<>
<ToggleButtonGroup
value={currentSelectedTheme}
@@ -681,7 +681,9 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Divider style={{ marginBottom: 10, }} />
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: 2.1.0-rc2
Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer">
2.1.0
</a>
</Typography>
</Menu>
</span>
+5 -1
View File
@@ -418,6 +418,10 @@ const LicencePopup = (props) => {
showSupport = true
}
if (userdata?.app_execution_limit >= 300000) {
top_text = "Enterprise Plan"
}
if (subscription.name.includes("Open Source")) {
top_text = "Open Source"
showSupport = true
@@ -761,7 +765,7 @@ const LicencePopup = (props) => {
{
isCloud ?
userdata?.app_execution_limit && userdata?.app_execution_limit !== 10000 ?
"You have already subscribed to the Scale plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." :
`You have already subscribed to the ${top_text}, which includes ${userdata?.app_execution_limit/1000}K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information.` :
`You are using free Starter plan with max ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} runs per month. Upgrade to increase this limit.`
:
@@ -0,0 +1,107 @@
import React, { useState, useEffect, useContext, memo, useMemo } from 'react'
import {getTheme} from '../theme.jsx';
import { Context } from '../context/ContextApi.jsx';
import {
Typography,
} from '@mui/material';
import {
BarChart,
BarSeries,
Bar,
BarLabel,
GridlineSeries,
Gridline,
TooltipArea,
ChartTooltip,
TooltipTemplate,
} from 'reaviz';
const LineChartWrapper = (props) => {
const {keys, inputname, height, width, border} = props
const [hovered, setHovered] = useState("");
const {themeMode} = useContext(Context)
const theme = getTheme(themeMode)
var inputdata = keys.data === undefined ? keys : keys.data
var newname = inputname === undefined || inputname === null ? "" : inputname.trim().replaceAll("_", " ")
newname = newname.charAt(0).toUpperCase() + newname.slice(1)
if (inputdata?.key !== undefined && inputdata?.key !== null && inputdata?.key !== "" && inputdata?.datasets !== undefined && inputdata?.datasets !== null && inputdata?.datasets.length > 0 && inputdata?.labels !== undefined && inputdata?.labels !== null && inputdata?.labels.length > 0) {
var tmpdata = inputdata?.datasets[0]
if (tmpdata?.data !== undefined && tmpdata?.data !== null && tmpdata?.data.length > 0 && inputdata?.labels?.length === tmpdata?.data?.length) {
console.log("Fix it!")
var newarray = []
for (var key in tmpdata.data) {
var entry = {
"key": inputdata.labels[key] !== undefined ? inputdata.labels[key] : key,
"data": tmpdata.data[key],
}
newarray.push(entry)
}
inputdata = newarray
}
}
if (inputdata === undefined || inputdata === null) {
return (
<Typography>
Invalid linegraph data format
</Typography>
)
}
var defaultStyle = {
color: "white",
padding: 30,
marginTop: 15,
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette?.borderRadius,
backgroundColor: theme.palette.platformColor,
}
if (border === false) {
defaultStyle.border = "none"
defaultStyle.borderRadius = 0
defaultStyle.backgroundColor = "transparent"
}
return (
<div style={defaultStyle}>
<Typography variant="h6" style={{marginBotton: 30, }}>
{newname}
</Typography>
<BarChart
style={{marginTop: 100, }}
width={"100%"}
height={height}
data={inputdata}
series={
<BarSeries
bar={
<Bar />
}
/>
}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
</div>
)
}
export default LineChartWrapper;
@@ -0,0 +1,7 @@
import React from 'react';
const LiveExecutionsGraph = ({ executions }) => {
return null
}
export default LiveExecutionsGraph
+12 -27
View File
@@ -49,7 +49,7 @@ const menuData = {
{
title: "Shuffle",
description:
"The most versatile automation engine with focus on security.",
"The most versatile automation engine. Focused on cybersecurity.",
icon: "/images/icons/shuffleLogo.svg",
path: "/docs/about",
gaData: {
@@ -61,7 +61,7 @@ const menuData = {
{
title: "Singul",
description:
"Connect your favorite services with a singul line of code.",
"Connect to your favorite services with a singul line of code.",
icon: "/images/logos/singul.svg",
path: "https://singul.io",
gaData: {
@@ -70,6 +70,7 @@ const menuData = {
label: "singul_click"
}
},
/*
{
title: "API Explorer",
description:
@@ -82,18 +83,19 @@ const menuData = {
label: "api_explorer_click"
}
},
*/
],
Services: [
{
title: "Professional Services",
title: "Proof of Concept",
description:
"Professional Services help you solve problems at your convenience.",
"Register for POC to test the best of Shuffle for free for 30 days.",
icon: "/images/ProfessionalServices.svg",
path: "/professional-services",
path: "/poc",
gaData: {
category: "navbar",
action: "services_click",
label: "professional_services_click"
label: "proof_of_concept_click"
}
},
{
@@ -101,7 +103,7 @@ const menuData = {
description:
"Support to help you build automations with confidence.",
icon: "/images/Support.svg",
path: "/contact?category=support",
path: "/support",
gaData: {
category: "navbar",
action: "services_click",
@@ -1245,23 +1247,6 @@ const Navbar = (props) => {
>
{menuItem.title}
</Typography>
{menuItem.title === "Singul" && (
<Typography
sx={{
fontSize: '10px',
color: '#FF8544',
border: '1px solid #FF8544',
borderRadius: '4px',
padding: '2px 6px',
lineHeight: 1,
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Beta: Coming Soon
</Typography>
)}
</Box>
<Typography
variant="body2"
@@ -1501,7 +1486,7 @@ const Navbar = (props) => {
}
}}
>
Become a partner
Become a Partner
</Button>
<Button
fullWidth
@@ -1531,7 +1516,7 @@ const Navbar = (props) => {
}
}}
>
Discover partners
Discover Partners
</Button>
</Box>
</Box>
@@ -1676,7 +1661,7 @@ const Navbar = (props) => {
const topbarHeight = showTopbar ? 40 : 0
const topbar = !isCloud || !showTopbar ? null :
curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/professional-services" ?
curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/poc" || curpath === "/professional-services" ?
<span style={{ zIndex: 50001, marginTop: -4}}>
{/* uncommit this to show topbar for release */}
{/* <div style={{ position: "relative", height: topbarHeight, backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", overflow: "hidden", }}>
@@ -56,7 +56,8 @@ const OrgHeaderexpandedNew = (props) => {
adminTab,
selectedStatus,
setSelectedStatus,
isEditOrgTab
isEditOrgTab,
handleStatusChange
} = props;
const classes = useStyles();
@@ -68,7 +69,7 @@ const OrgHeaderexpandedNew = (props) => {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 300,
borderRadius: 20,
borderRadius: 4,
overflowY: "scroll",
},
},
@@ -93,40 +94,6 @@ const OrgHeaderexpandedNew = (props) => {
const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
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
@@ -452,7 +419,7 @@ const OrgHeaderexpandedNew = (props) => {
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{color: theme.palette.text.primary}}>Name</Typography>
<Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Name</Typography>
<TextField
required
style={{
@@ -532,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
</div>
{userdata?.support ? (
<div style={{ alignItems: 'center' }}>
<div style={{ marginRight: '12px', color: theme.palette.text.primary }}>Status</div>
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
@@ -544,7 +511,7 @@ const OrgHeaderexpandedNew = (props) => {
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "integration partner", "distribution partner", "service partner", "old customer", "old lead"].map((name) => (
{["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "integration partner", "distribution partner", "channel partner", "service partner", "old customer", "old lead"].map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
@@ -558,13 +525,13 @@ const OrgHeaderexpandedNew = (props) => {
{isCloud ? (
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }} >
<Typography variant="text" style={{color: theme.palette.text.primary}}>Change Region</Typography>
<Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Change Region</Typography>
<RegionChangeModal selectedOrganization={selectedOrganization} setSelectedRegion={setSelectedRegion} userdata={userdata} handleSendChangeRegionMail={handleSendChangeRegionMail} />
</div>
) : null}
</div>
<div style={{ marginTop: "10px" }} />
<Typography variant="text" style={{color: theme.palette.text.primary}}>Description</Typography>
<Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Description</Typography>
<div style={{ display: "flex" }}>
<TextField
required
+11 -11
View File
@@ -36,7 +36,7 @@ const OrganizationTab = (props) => {
const [billingInfo, setBillingInfo] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const [curIndex, setCurIndex] = React.useState(0);
const items = ['Org Configuration', "SSO", "Notifications", 'Billing & Stats', 'Branding'];
const items = ['Org Configuration', "SSO", "Notifications", 'Billing & Stats'];
const [visibleTabs, setVisibleTabs] = useState(items);
const [unreadNotifications, setUnreadNotifications] = React.useState(
notifications?.filter((notification) => notification.read === false)?.length
@@ -157,16 +157,16 @@ const OrganizationTab = (props) => {
isLoaded={isLoaded}
/>
);
case 'branding':
return <Branding
isCloud={isCloud}
userdata={userdata}
globalUrl={globalUrl}
handleGetOrg={handleGetOrg}
selectedOrganization={selectedOrganization}
clickedFromOrgTab={true}
setSelectedOrganization={setSelectedOrganization}
/>;
// case 'branding':
// return <Branding
// isCloud={isCloud}
// userdata={userdata}
// globalUrl={globalUrl}
// handleGetOrg={handleGetOrg}
// selectedOrganization={selectedOrganization}
// clickedFromOrgTab={true}
// setSelectedOrganization={setSelectedOrganization}
// />;
// case 'analytics':
// return <AnalyticsTab isCloud={isCloud} userdata={userdata} globalUrl={globalUrl} />;
default:
+48 -29
View File
@@ -150,6 +150,7 @@ const ParsedAction = (props) => {
globalUrl,
setSelectedActionEnvironment,
requiresAuthentication,
setRequiresAuthentication,
hideExtraTypes,
scrollConfig,
setScrollConfig,
@@ -904,13 +905,12 @@ const ParsedAction = (props) => {
if (paramvalue.includes("$")) {
let actions = workflow.actions?.map((action) => {
return "$" + action.label?.toLowerCase();
return "$" + action.label?.toLowerCase().replaceAll(" ", "_");
})
if (newActionList?.length > 0) {
let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase());
let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase().replaceAll(" ", "_"));
let notPresentAction = actions?.filter((action) => !appParentActions?.includes(action))
// Extract all variable references from paramvalue
// Examples of what it matches:
// - Simple variables: $test, $myVar, $x
@@ -1077,8 +1077,8 @@ const ParsedAction = (props) => {
var toReplace = event.target.value
if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) {
toReplace = toReplace.replaceAll('\\"', '"').replaceAll('"', '\\"')
if (!toReplace?.startsWith("{") && !toReplace?.startsWith("[")) {
toReplace = toReplace?.replaceAll('\\"', '"').replaceAll('"', '\\"')
}
if (
@@ -1235,7 +1235,7 @@ const ParsedAction = (props) => {
console.log("APIKEY - this shouldn't show up!")
}
if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && data.name === "input_list") {
if (selectedAction.app_name === "Shuffle Tools" && (selectedAction.name === "filter_list" || selectedAction.name === "is_in_datastore") && data.name === "input_list") {
//console.log("FILTER LIST!: ", event, count, data)
const parsedvalue = event.target.value
if (parsedvalue.includes(".#")) {
@@ -1251,7 +1251,7 @@ const ParsedAction = (props) => {
selectedAction.parameters[1].value = splitparsed[1]
if (splitparsed.length >= 2) {
toast.warn("Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", {
toast.warn("Datastore checker/Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", {
autoClose: 10000,
})
} else if (selectedAction.parameters[1].value.includes(".#")) {
@@ -1294,7 +1294,7 @@ const ParsedAction = (props) => {
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
if (paramcheck !== undefined) {
// Escapes all double quotes
const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\"");
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"] = [{
@@ -2057,14 +2057,14 @@ const ParsedAction = (props) => {
value={appActionName}
onChange={(event) => {
let newValue = event.target.value
newValue = newValue.replaceAll(" ", "_")
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) {
@@ -2289,9 +2289,9 @@ const ParsedAction = (props) => {
)}
{selectedApp.name !== undefined &&
selectedAction.authentication !== null &&
((selectedAction.authentication !== null &&
selectedAction.authentication !== undefined &&
selectedAction.authentication.length === 0 &&
selectedAction.authentication.length === 0) || isAgent || isIntegration) &&
requiresAuthentication ? (
<div style={{ marginTop: 15 }}>
<Tooltip
@@ -2304,6 +2304,7 @@ const ParsedAction = (props) => {
color="primary"
style={{
textTransform: "none",
fontWeight: "bold",
}}
fullWidth
variant="contained"
@@ -2316,7 +2317,7 @@ const ParsedAction = (props) => {
}}
>
<AddIcon style={{ marginRight: 10 }} /> Authenticate{" "}
{selectedApp.name.replaceAll("_", " ")}
{isAgent || isIntegration ? "API" : selectedApp.name?.replaceAll("_", " ")}
</Button>
</span>
</Tooltip>
@@ -2814,7 +2815,7 @@ const ParsedAction = (props) => {
}}
filterOptions={(options, { inputValue }) => {
const lowercaseValue = inputValue.toLowerCase()
options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue))
options = options.filter(x => x.name?.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description?.toLowerCase().includes(lowercaseValue))
return options
}}
@@ -2824,8 +2825,8 @@ const ParsedAction = (props) => {
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
option.name?.charAt(0).toUpperCase() + option.name?.substring(1)
)?.replaceAll("_", " ");
return newname;
}}
@@ -2877,7 +2878,7 @@ const ParsedAction = (props) => {
option.label = "No name"
}
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " ");
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1))?.replaceAll("_", " ");
var method = ""
var extraDescription = ""
@@ -3152,8 +3153,20 @@ const ParsedAction = (props) => {
setSelectedAction(selectedAction)
setUpdate(Math.random())
var requiresAuth = app?.authentication?.required
if (requiresAuth && appAuthentication?.length > 0) {
for (var key in appAuthentication) {
if (appAuthentication[key]?.app?.name === app?.name) {
requiresAuth = false
break
}
}
}
setRequiresAuthentication(requiresAuth);
}}>
<Tooltip title={`Select ${app.name.replaceAll("_", " ")}`} placement="top">
<Tooltip title={`Select ${app.name?.replaceAll("_", " ")}`} placement="top">
<img
src={app.large_image}
style={{
@@ -3228,8 +3241,8 @@ const ParsedAction = (props) => {
}
const newname = (
option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1)
).replaceAll("_", " ");
option.app_name?.charAt(0).toUpperCase() + option.app_name?.substring(1)
)?.replaceAll("_", " ");
return newname;
}}
options={selectedAction.matching_actions}
@@ -3263,8 +3276,8 @@ const ParsedAction = (props) => {
newActionname = (
newActionname.charAt(0).toUpperCase() +
newActionname.substring(1)
).replaceAll("_", " ");
newActionname?.substring(1)
)?.replaceAll("_", " ");
return (
<div style={{ display: "flex" }}>
@@ -3506,6 +3519,8 @@ const ParsedAction = (props) => {
if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") {
// Show a key popout button
showCacheConfig = true
} else if (data.name === "category" && selectedAction.app_name === "Shuffle Tools") {
showCacheConfig = true
}
var disabled = false;
@@ -3733,6 +3748,7 @@ const ParsedAction = (props) => {
}
const clickedFieldId = "rightside_field_" + count;
const parameterFieldId = "param_" + data.name.replace(/[^a-zA-Z0-9_-]/g, '_');
var baseHelperText = ""
if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) {
@@ -3749,8 +3765,8 @@ const ParsedAction = (props) => {
}
tmpitem = (
tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1)
).replaceAll("_", " ");
tmpitem?.charAt(0).toUpperCase() + tmpitem?.substring(1)
)?.replaceAll("_", " ");
if (tmpitem === "Username basic") {
tmpitem = "Username"
@@ -3874,6 +3890,9 @@ const ParsedAction = (props) => {
autofill="off"
autoComplete="off"
id={clickedFieldId}
data-parameter={data.name}
data-param-id={parameterFieldId}
name={data.name}
disabled={disabled}
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
@@ -4316,7 +4335,7 @@ const ParsedAction = (props) => {
viewed_data = split_data[0]
}
viewed_data = (viewed_data.charAt(0).toUpperCase() + viewed_data.slice(1)).replaceAll("_", " ")
viewed_data = (viewed_data?.charAt(0).toUpperCase() + viewed_data?.slice(1))?.replaceAll("_", " ")
// Check if it's selected or not and highlight
var selected = false
@@ -4379,9 +4398,9 @@ const ParsedAction = (props) => {
? values[0].autocomplete
: "$" + values[0].autocomplete;
toComplete = toComplete.toLowerCase().replaceAll(" ", "_");
toComplete = toComplete?.toLowerCase()?.replaceAll(" ", "_");
for (let [key, keyval] in Object.entries(values)) {
if (key == 0 || values[key].autocomplete.length === 0) {
if (key == 0 || values[key]?.autocomplete?.length === 0) {
continue;
}
@@ -4731,7 +4750,7 @@ const ParsedAction = (props) => {
);
}
const buttonTitle = `Authenticate API ${selectedApp.name.replaceAll("_", " ")}`
const buttonTitle = `Authenticate the ${selectedApp?.name?.replaceAll("_", " ")} API`
const hasAutocomplete = data?.autocompleted === true
if (data.variant === undefined || data.variant === null) {
data.variant = "STATIC_VALUE"
+89 -15
View File
@@ -222,7 +222,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Name
</Typography>
<Skeleton
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
/>
</div> */}
<div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Solutions
</div>
<Skeleton
@@ -282,7 +282,7 @@ const PartnerDetails = (props) => {
/>
</div>
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Region
</Typography>
<Skeleton
@@ -297,7 +297,7 @@ const PartnerDetails = (props) => {
/>
</div>
<div style={{ alignItems: "center", marginLeft: 12 }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Country
</div>
<Skeleton
@@ -313,7 +313,7 @@ const PartnerDetails = (props) => {
</div>
</div>
<div style={{ marginTop: "10px" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Description
</Typography>
<Skeleton
@@ -329,7 +329,7 @@ const PartnerDetails = (props) => {
</div>
<div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Website URL
</Typography>
<Skeleton
@@ -344,7 +344,7 @@ const PartnerDetails = (props) => {
/>
</div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Article URL
</Typography>
<Skeleton
@@ -358,6 +358,21 @@ const PartnerDetails = (props) => {
animation="wave"
/>
</div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Contact Email
</Typography>
<Skeleton
variant="rounded"
height={35}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
</div>
</div>
</div>
@@ -382,7 +397,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Name
</Typography>
@@ -529,6 +544,7 @@ const PartnerDetails = (props) => {
style={{
marginRight: "12px",
color: theme.palette.text.primary,
fontFamily: theme?.typography?.fontFamily
}}
>
Solutions
@@ -571,7 +587,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Region
</Typography>
@@ -587,7 +603,7 @@ const PartnerDetails = (props) => {
<div style={{ alignItems: "flex-start", marginLeft: 13, display: "flex", flexDirection: "column" }}>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Country
</Typography>
@@ -671,7 +687,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: "10px", }} />
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Description
</Typography>
@@ -731,7 +747,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Website URL
</Typography>
@@ -794,7 +810,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Article URL
</Typography>
@@ -824,7 +840,7 @@ const PartnerDetails = (props) => {
value={partnerData?.article_url}
onBlur={() => {}}
onChange={(e) => {
if (e.target.value.length > 100) {
if (e.target.value.length > 1000) {
toast("Choose a shorter article URL.");
return;
}
@@ -851,7 +867,65 @@ const PartnerDetails = (props) => {
},
}}
/>
</div>
</div>
<div
style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Contact Email
</Typography>
<TextField
required
disabled={isDisabled}
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 500,
marginTop: "5px",
marginRight: "15px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="support@shuffler.io"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={partnerData?.contact_email}
onBlur={() => {}}
onChange={(e) => {
setPartnerData({
...partnerData,
contact_email: e.target.value,
});
}}
color="primary"
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: "35px",
fontSize: "1em",
borderRadius: 4,
backgroundColor:
theme.palette.textFieldStyle.backgroundColor,
},
classes: {
notchedOutline: isEditOrgTab
? null
: classes.notchedOutline,
},
}}
/>
</div>
</div>
</div>
</div>
+2 -2
View File
@@ -741,7 +741,7 @@ const PartnerHeader = (props) => {
<div >
<Button
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
variant="contained"
variant="outlined"
color="primary"
disabled={isDisabled || isPublishing}
onClick={() => {
@@ -812,7 +812,7 @@ const PartnerHeader = (props) => {
<div >
<Button
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
variant="contained"
variant="outlined"
color="primary"
disabled={isDisabled || isPublishing}
onClick={() => {
+53 -36
View File
@@ -42,7 +42,7 @@ const PartnerSettings = (props) => {
// Partner Types handling : Getting from org status
useEffect(() => {
const partnerTypes = {};
userdata?.org_status.forEach(status => {
userdata?.org_status?.forEach(status => {
if (status.includes("_partner")) {
partnerTypes[status] = true;
}
@@ -54,7 +54,8 @@ const PartnerSettings = (props) => {
"tech_partner": "#ff8544",
"distribution_partner": "#2BC07E",
"service_partner": "#a99cf9",
"integration_partner": "#fb47a0"
"integration_partner": "#fb47a0",
"channel_partner": "#4caf50",
}
const handleSendUpdateRequest = () => {
@@ -85,6 +86,7 @@ const PartnerSettings = (props) => {
{ field: partnerData?.landscape_image_url, name: "Landscape Image" },
{ field: partnerData?.website_url, name: "Website URL" },
{ field: partnerData?.article_url, name: "Article URL" },
{ field: partnerData?.contact_email, name: "Contact Email" },
{ field: partnerData?.country, name: "Country" },
{ field: partnerData?.region, name: "Region" },
];
@@ -119,6 +121,12 @@ const PartnerSettings = (props) => {
toast.error("There should be at least one partner type");
return;
}
// Validate description length
if (partnerData?.description && partnerData.description.length > 1000) {
toast.error("Description should be less than or equal to 1000 characters");
return;
}
setIsPublishing(true);
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
@@ -129,6 +137,7 @@ const PartnerSettings = (props) => {
description: partnerData.description?.trim(),
website_url: partnerData.website_url?.trim(),
article_url: partnerData.article_url?.trim(),
contact_email: partnerData.contact_email?.trim(),
partner_type: partnerTypes,
expertise: partnerData?.expertise || [],
services: partnerData?.services || [],
@@ -150,17 +159,15 @@ const PartnerSettings = (props) => {
})
.then((response) => {
setIsPublishing(false);
if (response.status !== 200) {
if (response.status === 200) {
toast.success("Partner details successfully updated");
} else {
toast.error("Failed to publish partner");
}
return response.json();
})
.then((responseJson) => {
toast.success("Partner details successfully updated");
})
.catch((error) => {
setIsPublishing(false);
toast.error("Failed to update partner details: " + error?.message);
toast.error("Failed to update partner details: " + error?.reason);
})
}
@@ -177,6 +184,7 @@ const PartnerSettings = (props) => {
description: partnerData.description?.trim(),
website_url: partnerData.website_url?.trim(),
article_url: partnerData.article_url?.trim(),
contact_email: partnerData.contact_email?.trim(),
partner_type: partnerTypes,
usecases: partnerData?.usecases || [],
expertise: partnerData?.expertise || [],
@@ -233,32 +241,41 @@ const PartnerSettings = (props) => {
sx={{display:"flex", alignItems:"flex-start", gap:2, justifyContent:"flex-start"
}}>
<Typography variant='h3' sx={{ marginBottom: "15px", marginTop: 0, }}>Configuration</Typography>
{Object?.entries(partnerTypes)?.map(([key, value]) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "999px",
py: 1.2,
px: 2.5,
fontSize: "13px",
fontWeight: 500,
fontFamily: theme.typography.fontFamily,
color: "#fff",
backgroundColor: "transparent",
border: `1.5px solid ${
partnerTypeColors[key]
}`,
transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())}
</Box>
))}
{Object?.entries(partnerTypes)
?.filter(([key, value]) => key !== "distribution_partner")
?.map(([key, value]) => {
let displayText = key?.replace("_", " ")?.replace(/\b\w/g, char => char.toUpperCase());
if (displayText === "Tech Partner") {
displayText = "Technology Partner";
}
return (
<Box
key={key}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "999px",
py: 1.2,
px: 2.5,
fontSize: "13px",
fontWeight: 500,
fontFamily: theme.typography.fontFamily,
color: "#fff",
backgroundColor: "transparent",
border: `1.5px solid ${
partnerTypeColors[key]
}`,
transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{displayText}
</Box>
);
})}
</Box>
<Box
sx={{
@@ -314,9 +331,9 @@ const PartnerSettings = (props) => {
boxShadow: "none",
marginRight: 4,
px: 3,
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50",
backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E",
"&:hover": {
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50"
backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E"
}
}}
variant="contained"
+27 -5
View File
@@ -66,18 +66,17 @@ const PartnerTab = (props) => {
})
.then((response) => {
if (response.status !== 200) {
toast("Failed to get partner data")
toast.info("No partner details found");
}
return response.json();
})
.then((responseJson) => {
if(responseJson.success) {
setPartnerData(responseJson?.partner);
console.log("responseJson", responseJson)
setLoadingPartnerData(false);
}else{
setLoadingPartnerData(false);
toast(responseJson?.reason)
console.error(responseJson?.reason)
}
})
.catch((error) => {
@@ -195,14 +194,17 @@ const PartnerTab = (props) => {
// Enable the tab by default
return false;
}
const isSupportOnlyTab = (tabName) => {
return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents";
}
return (
<div style={{ height: "100%", width: "100%", color: theme.palette.platformColor, backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, boxSizing: 'border-box' }}>
<div style={{ display: 'flex', justifyContent: 'space-around', width: "100%", borderBottom: theme.palette.defaultBorder ,boxSizing: 'border-box' }}>
{tabsOnPartnerTab?.map((tabName, index) => (
<div style={{ pointerEvents: 'auto', width: '100%',}}>
<div key={tabName} style={{ pointerEvents: 'auto', width: '100%', position: 'relative'}}>
<Button
key={tabName}
onClick={() => {
setCurIndex(index);
handleTabClick(index === 0 ? "partner_settings" : tabName.toLowerCase().replace(/[\s&]+/g, ''));
@@ -232,6 +234,26 @@ const PartnerTab = (props) => {
>
{tabName}
</Button>
{isSupportOnlyTab(tabName) && userdata?.support && (
<div style={{
position: 'absolute',
top: '8px',
right: '8px',
backgroundColor: '#4D4D4D',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 'bold',
zIndex: 1
}}>
S
</div>
)}
</div>
))}
</div>
@@ -29,9 +29,9 @@ import CloseIcon from "@mui/icons-material/Close";
import { toast } from "react-toastify";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import EditIcon from "@mui/icons-material/Edit";
import StarIcon from "@mui/icons-material/Star";
import DeleteIcon from "@mui/icons-material/Delete";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
@@ -1083,16 +1083,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
<FormControl>
<Typography
<Box
sx={{
color: theme.palette.accentColor,
display: "flex",
flexDirection: "row",
gap: 1,
mb: 2,
fontSize: "16px",
fontWeight: 600,
alignItems: "center",
}}
>
Public Workflow
</Typography>
<Typography
sx={{
color: theme.palette.accentColor,
fontSize: "16px",
fontWeight: 600,
}}
>
Public Workflow
</Typography>
<IconButton
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={() => {
// Open workflow in new tab - adjust URL as needed
window.open(`${window.location.origin}/workflows/${formData.mainContent.publicWorkflowId}`, "_blank");
}}
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Box>
<Select
displayEmpty
value={
+5 -4
View File
@@ -176,11 +176,12 @@ const Priorities = memo((props) => {
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
toast(`Failed getting config for ${item.id}: `, response.reason);
//toast.error(`Failed getting config for ${item.id}: `, response.reason)
console.log("Status not 200 for app config :O!");
return;
return
}
return response.json();
return response.json()
}).then((responseJson) => {
if (!responseJson.success) {
console.log("Could not get app config")
@@ -209,7 +210,7 @@ const Priorities = memo((props) => {
}).catch((error) => {
console.log("Error getting app config: " + error);
toast("Error getting app config: " + error);
//toast.error("Error getting app config: " + error);
})
})
})
+65 -22
View File
@@ -79,9 +79,14 @@ const RuntimeDebugger = (props) => {
const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(10)
const [maxExecutionCount, setMaxExecutionCount] = useState(50)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [suborgWorkflowRuns, setSuborgWorkflowRuns] = useState(false)
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 10,
})
const [openWorkflowMenu, setOpenWorkflowMenu] = useState(false)
const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",}
@@ -665,6 +670,13 @@ const RuntimeDebugger = (props) => {
},
]
useEffect(() => {
setPaginationModel(prev => ({
...prev,
pageSize: rowsPerPage
}))
}, [rowsPerPage])
useEffect(() => {
// Check if the user is currently focusing a texxtfield or not
// If they are, don't submit the search
@@ -672,7 +684,7 @@ const RuntimeDebugger = (props) => {
return
}
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}, [workflowId, status, startTime, endTime])
const textfieldStyle = {
@@ -723,7 +735,7 @@ const RuntimeDebugger = (props) => {
setWorkflowId(e.target.value.id)
setSuborgWorkflowRuns(false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, rowsPerPage, false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, maxExecutionCount, false)
}
const executeWorkflow = (execution) => {
@@ -819,9 +831,12 @@ const RuntimeDebugger = (props) => {
<div style={{display: "flex", paddingTop: 50, }}>
<div style={{display: 'flex', flexDirection: 'column'}}>
<div style={{display: "flex", width: "100%", }}>
<Typography variant="h3" style={{flex: 3, whiteSpace: "nowrap" }}>Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}</Typography>
<Typography variant="h3" style={{flex: 3, whiteSpace: "nowrap" }}>
Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}
</Typography>
{selectedWorkflowExecutions.length > 0 ?
<ButtonGroup>
<ButtonGroup>
<Tooltip title="Reruns ALL selected workflows. This will make a new execution for them, and not continue the existing.">
<Button
variant="outlined"
@@ -864,7 +879,7 @@ const RuntimeDebugger = (props) => {
} else {
toast("Aborted "+aborted+" workflows.")
// Research
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
setSelectedWorkflowExecutions([])
}
@@ -902,7 +917,7 @@ const RuntimeDebugger = (props) => {
marginTop: 20,
marginLeft: 10,
marginRight: 12,
width: 693,
width: 643,
height: 51,
borderRadius: 4,
fontSize: 16,
@@ -913,7 +928,7 @@ const RuntimeDebugger = (props) => {
color: theme.palette.textColor,
fontSize: "1em",
height: 51,
width: 693,
width: 643,
borderRadius: 4,
},
startAdornment: (
@@ -942,6 +957,34 @@ const RuntimeDebugger = (props) => {
placeholder="Filter by Workflow Name, Status, Execution Argument, Results"
id="shuffle_search_field"
/>
<Tooltip title="Set the maximum number of workflow executions to retrieve in search results" placement="top">
<FormControl style={{ minWidth: 120, marginTop: 20 }}>
<InputLabel id="max-execution-count-label" style={{ fontSize: '0.875rem' }}>Max Results</InputLabel>
<Select
labelId="max-execution-count-label"
id="max-execution-count-select"
value={maxExecutionCount}
label="Max Results"
size="small"
onChange={(e) => {
setMaxExecutionCount(e.target.value)
submitSearch(workflowId, status, startTime, endTime, rowCursor, e.target.value, suborgWorkflowRuns)
}}
style={{
height: 50,
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
}}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={200}>200</MenuItem>
<MenuItem value={500}>500</MenuItem>
</Select>
</FormControl>
</Tooltip>
</div>
{userdata?.active_org?.creator_org?.length === 0 ? (
<div style={{display: "flex", margin: 'auto',marginTop: 20,justifyContent: 'center', alignItems: 'center', }}>
@@ -956,7 +999,8 @@ const RuntimeDebugger = (props) => {
setStartTime("")
setEndTime("")
setSearchQuery("")
submitSearch("", "", "", "", rowCursor, rowsPerPage, !suborgWorkflowRuns)}
setMaxExecutionCount(50)
submitSearch("", "", "", "", rowCursor, 50, !suborgWorkflowRuns)}
}
color="secondary"
/>
@@ -967,7 +1011,7 @@ const RuntimeDebugger = (props) => {
</div>
</div>
<form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}} style={{display: "flex", justifyContent: "center", alignItems: "center", }}>
<FormControl fullWidth style={{marginTop: 5, }}>
<InputLabel id="status-label">Status</InputLabel>
@@ -1158,7 +1202,8 @@ const RuntimeDebugger = (props) => {
setEndTime("")
setSearchQuery("")
setSuborgWorkflowRuns(false)
submitSearch("", "", "", "", rowCursor, rowsPerPage, false)
setMaxExecutionCount(50)
submitSearch("", "", "", "", rowCursor, 50, false)
}}
>
<FilterAltOffIcon />
@@ -1169,7 +1214,7 @@ const RuntimeDebugger = (props) => {
variant="outlined"
color="primary"
onClick={() => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}}
disabled={searchLoading}
style={{height: 50, minWidth: 100, marginTop: 15, }}
@@ -1181,20 +1226,18 @@ const RuntimeDebugger = (props) => {
<DataGrid
rows={filteredRows}
columns={columns}
pageSize={rowsPerPage}
rowsPerPageOptions={[10, 20, 50, 100]}
paginationModel={paginationModel}
pageSizeOptions={[10, 20, 50, 75, 100]}
checkboxSelection
disableSelectionOnClick
onPageSizeChange={(newPageSize) => {
setRowsPerPage(newPageSize)
submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize, suborgWorkflowRuns)
onPaginationModelChange={(newPaginationModel) => {
setPaginationModel(newPaginationModel)
setRowsPerPage(newPaginationModel.pageSize)
// No API call needed - this is just for client-side pagination
}}
// event for when clicking next page
// Hide page changer
onPageChange={(params) => {
console.log("params: ", params)
}}
onSelectionModelChange={(newSelection) => {
onRowSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection)
var found = []
+207 -32
View File
@@ -47,6 +47,8 @@ import {
RestartAlt as RestartAltIcon,
ArrowForward as ArrowForwardIcon,
KeyboardReturn as KeyboardReturnIcon,
FormatIndentIncrease as FormatIndentIncreaseIcon,
Fullscreen as FullscreenIcon,
} from '@mui/icons-material';
@@ -143,6 +145,34 @@ const CodeEditor = (props) => {
handleConditionFieldChange,
} = props
// Auto-indent JSON-like content (with safety hehe)
const autoIndentContent = React.useCallback((content) => {
// Safety checks :)
if (!content || typeof content !== 'string' || content.trim().length === 0) {
return content;
}
try {
// Check if content looks like JSON (starts with { or [)
const trimmedContent = content.trim();
if (trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) {
try {
const parsed = JSON.parse(trimmedContent);
return IndentJsonLikeString(JSON.stringify(parsed), 2);
} catch (parseError) {
return IndentJsonLikeString(content, 2);
}
}
// Return original content if it doesn't look like JSON
return content;
} catch (error) {
// If anything goes wrong, return original content
console.warn('Auto-indent failed, using original content:', error);
return content;
}
}, []);
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
// const {codelang, setcodelang} = props
@@ -184,6 +214,7 @@ const CodeEditor = (props) => {
"result": baseResult,
})
const [executing, setExecuting] = useState(false)
const [fullScreenModeEnabled, setFullScreenModeEnabled] = useState(fullScreenMode === true || fullScreenMode === "true" || localStorage.getItem("codeEditorFullScreen") === "true")
const liquidOpen = Boolean(anchorEl);
const mathOpen = Boolean(anchorEl2);
@@ -200,6 +231,22 @@ const CodeEditor = (props) => {
expectedOutput(localcodedata)
}, [localcodedata])
// Auto-indent when codedata prop changes
useEffect(() => {
if (codedata && codedata !== localcodedata && typeof codedata === 'string') {
try {
const indentedContent = autoIndentContent(codedata);
if (indentedContent !== undefined && indentedContent !== null) {
setlocalcodedata(indentedContent);
}
} catch (error) {
console.warn('Failed to auto-indent codedata:', error);
// Fallback to original codedata
setlocalcodedata(codedata);
}
}
}, [codedata, autoIndentContent])
let navigate = useNavigate();
const [searchParams] = useSearchParams();
const actionId = searchParams.get('action_id');
@@ -217,7 +264,13 @@ const CodeEditor = (props) => {
}
const action = workflow?.actions?.find(action => action.id === actionId);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent action data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedAction(action);
// Update available variables when action changes
@@ -230,7 +283,13 @@ const CodeEditor = (props) => {
}
const trigger = workflow?.triggers?.find(trigger => trigger.id === triggerId);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent trigger data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedTrigger(trigger);
// Update available variables when trigger changes
@@ -244,7 +303,13 @@ const CodeEditor = (props) => {
}
const condition = selectedEdge?.conditions?.find(condition => condition.id === conditionId);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent condition data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedCondition(condition);
// Update available variables when condition changes
updateAvailableVariables(actionlist);
@@ -963,7 +1028,6 @@ const CodeEditor = (props) => {
// vs
// $variable.#.subvalue
// if you put both of those lines in the same editor, then it will replace both (somehow). Make sure $variable.#.subvalue exists while testing.
console.log("FOUNDLOC: ", fixedVariable, foundlocation)
for (var j = 0; j < actionlist.length; j++) {
if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) {
continue
@@ -990,7 +1054,6 @@ const CodeEditor = (props) => {
}
try {
console.log("REPLACE: ", foundlocation, fixedVariable, newvalue)
if (newvalue !== "") {
if (foundlocation === -1) {
input = input.replace(fixedVariable, newvalue, 1)
@@ -1303,7 +1366,7 @@ const CodeEditor = (props) => {
editor.completers = [customCompleter]
}
if (fullScreenMode) {
if (fullScreenMode && fullScreenModeEnabled === true) {
return (
<AceEditor
mode="python"
@@ -1454,6 +1517,60 @@ const CodeEditor = (props) => {
)
}
const IndentJsonLikeString = (input, indentSize = 2) => {
const indent = ' '.repeat(indentSize);
let level = 0;
let inString = false;
let escapeNext = false;
let result = '';
for (let i = 0; i < input.length; i++) {
let char = input[i];
if (escapeNext) {
result += char;
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
result += char;
continue;
}
if (char === '"') {
inString = !inString;
result += char;
continue;
}
if (!inString) {
if (char === '{' || char === '[') {
result += char + '\n' + indent.repeat(++level);
continue;
} else if (char === '}' || char === ']') {
result += '\n' + indent.repeat(--level) + char;
continue;
} else if (char === ',') {
result += char + '\n' + indent.repeat(level);
continue;
} else if (char === ':') {
result += ': ';
continue;
} else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
// Skip whitespace characters when not in string
continue;
}
}
result += char;
}
return result;
}
const SourceDataOption = (option) => {
const { innerdata, parsedPaths, defaultExpanded } = option
@@ -1526,15 +1643,15 @@ const CodeEditor = (props) => {
// zIndex: 12501,
pointerEvents: "auto",
color: theme.palette.DialogStyle.color,
minWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor ? "100%" : "700px",
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
border: "3px solid rgba(255,255,255,0.3)",
padding: isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px",
padding: fullScreenModeEnabled ? "50px 0px 20px 200px" : isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px",
backgroundColor: themeMode === "dark" ? "black" : theme.palette.DialogStyle.backgroundColor,
opacity: isWorkflowEditor ? 0.93 : 1,
opacity: isWorkflowEditor || fullScreenModeEnabled ? 0.93 : 1,
},
}}
>
@@ -1549,39 +1666,66 @@ const CodeEditor = (props) => {
</Tooltip>
: null}
{fullScreenModeEnabled ? null :
<Tooltip
color="primary"
title={`Move window`}
placement="left"
>
<IconButton
id="draggable-dialog-title"
style={{
zIndex: 5000,
position: "absolute",
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 166 : 66,
color: "grey",
cursor: "move",
}}
onClick={() => {
}}
>
<DragIndicatorIcon />
</IconButton>
</Tooltip>
}
<Tooltip
color="primary"
title={`Move window`}
placement="left"
title={fullScreenModeEnabled ? `Exit Fullscreen Mode` : `Enter Fullscreen Mode`}
placement="top"
>
<IconButton
id="draggable-dialog-title"
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 56,
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 136 : 36,
color: "grey",
cursor: "move",
}}
onClick={() => {
setFullScreenModeEnabled(!fullScreenModeEnabled)
localStorage.setItem("codeEditorFullScreen", !fullScreenModeEnabled)
}}
>
<DragIndicatorIcon />
{!fullScreenModeEnabled ?
<FullscreenIcon />
:
<FullscreenExitIcon />
}
</IconButton>
</Tooltip>
<Tooltip
color="primary"
title={`Close window without saving`}
placement="left"
placement="right"
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 6,
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 106 : 6,
color: "grey",
}}
onClick={() => {
@@ -2127,6 +2271,30 @@ const CodeEditor = (props) => {
marginLeft: 100,
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) {
setlocalcodedata(indentedText)
} else {
toast.warn("Could not indent the text. Please check the input format.", { autoClose: 5000 })
}
}}
color="secondary"
>
<Tooltip
title={"Indent Text"}
placement="top"
>
<FormatIndentIncreaseIcon />
</Tooltip>
</IconButton>
<IconButton
style={{
height: 50,
width: 50,
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => {
if (fixExample !== undefined) {
const newExample = fixExample(editorData.example)
@@ -2196,14 +2364,15 @@ const CodeEditor = (props) => {
value={localcodedata}
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox"
height={isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : isWorkflowEditor ? "90vw" : "100%"}
height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
markers={markers}
highlightActiveLine={false}
enableBasicAutocompletion={true}
completers={[customCompleter]}
showPrintMargin={false}
style={{
wordBreak: "break-word",
@@ -2351,8 +2520,9 @@ const CodeEditor = (props) => {
style={{
border: `1px solid rgba(255, 255, 255, 0.15)`,
position: "absolute",
top: 20,
right: 100,
top: fullScreenModeEnabled ? 50 : 20,
right: fullScreenModeEnabled ? 240 : 120,
maxHeight: 35,
minWidth: 70,
zIndex: 1200,
@@ -2466,8 +2636,8 @@ const CodeEditor = (props) => {
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 190,
minheight: 190,
maxHeight: fullScreenModeEnabled ? 300 : 190,
minheight: fullScreenModeEnabled ? 300 : 190,
overflow: "auto",
}}
collapsed={false}
@@ -2476,9 +2646,14 @@ const CodeEditor = (props) => {
}}
displayDataTypes={false}
onSelect={(select) => {
//HandleJsonCopy(executionResult.result, select, "exec");
var basename = "exec"
if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) {
basename = selectedAction.label.toLowerCase().replaceAll(" ", "_")
}
HandleJsonCopy(executionResult.result, select, basename)
}}
name={"Test result"}
name={"Run Output"}
/>
:
<span style={{ maxHeight: 190, minHeight: 190, }}>
@@ -2523,7 +2698,7 @@ const CodeEditor = (props) => {
</div>
<div style={{ display: 'flex', width: isWorkflowEditor ? "90%" : "100%", }}>
<div style={{ display: 'flex', width: fullScreenModeEnabled ? "92%" : isWorkflowEditor ? "90%" : "100%", }}>
<Button
style={{
height: 35,
+3 -4
View File
@@ -667,13 +667,12 @@ const TenantsTab = memo((props) => {
const handleDeleteAccount = () => {
const baseURL = globalUrl;
const url = `${baseURL}/api/v1/orgs/${selectedOrganization?.id}`;
const url = `${baseURL}/api/v1/orgs/${selectedSuborg?.id}`;
const data = {
suborg_id : selectedSuborg?.id,
password: password,
}
};
fetch(url, {
mode: "cors",
method: "DELETE",
+5 -1
View File
@@ -1465,7 +1465,11 @@ const UserManagmentTab = memo((props) => {
<ListItem key={index} style={{ backgroundColor: bgColor, display: 'table-row', borderBottomLeftRadius: users?.length - 1 === index ? 8 : 0, borderBottomRightRadius: users?.length - 1 === index ? 8 : 0 }}>
{isCloud ? (
<ListItemText
primary={(<img src={`https://flagcdn.com/48x36/${userRegion.toLowerCase()}.png`} alt={data?.user_geo_info?.country?.iso_code} style={{ marginRight: 30, width: 25, height: 23, }} />)}
primary={(
userRegion ? (
<img src={`https://flagcdn.com/48x36/${userRegion.toLowerCase()}.png`} alt={data?.user_geo_info?.country?.iso_code} style={{ marginRight: 30, width: 25, height: 23, }} />
) : null
)}
style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'center' }}
/>) : null}
<ListItemText
@@ -0,0 +1,275 @@
import React, { useEffect, useState } from "react";
import { getTheme } from '../theme.jsx';
import { toast } from "react-toastify"
import {
Button,
Dialog,
IconButton,
Typography,
CircularProgress,
Tooltip,
TextareaAutosize,
TextField,
ButtonGroup,
} from "@mui/material";
import {
Close as CloseIcon,
DragIndicator as DragIndicatorIcon,
Send as SendIcon,
} from '@mui/icons-material';
const WorkflowGenerationModal = (props) => {
const {
open = false,
supportEmail = "support@shuffler.io",
isMobile = false,
theme = null,
workflow={},
setWorkflow = () => {},
saveWorkflow = () => {},
setWorkflowGenerationModalOpen = () => {},
isCloud = false,
globalUrl = "",
} = props;
const [isFocused, setIsFocused] = useState(false);
const [workflowDescription, setWorkflowDescription] = useState("");
const [isAiEditing, setIsAiEditing] = React.useState(false);
const [backupWorkflow, setBackupWorkflow] = React.useState(null);
const currentTheme = theme || getTheme("dark");
const hasBackup = backupWorkflow !== null && backupWorkflow !== undefined
const handleKeyDown = (event) => {
if (open === false) {
return
}
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault()
const tryItButton = document.getElementById("try-it-button")
if (tryItButton !== undefined && tryItButton !== null) {
tryItButton.click()
} else {
const keepChangesButton = document.getElementById("keep-changes-button")
if (keepChangesButton !== undefined && keepChangesButton !== null) {
keepChangesButton.click()
}
}
}
// Ctrl+Z
if ((event.metaKey || event.ctrlKey) && event.key === 'z') {
event.preventDefault()
const discardChangesButton = document.getElementById("discard-changes-button")
if (discardChangesButton !== undefined && discardChangesButton !== null) {
discardChangesButton.click()
}
}
// Escape
if (event.key === 'Escape') {
setWorkflowDescription("");
setIsAiEditing(false);
setWorkflowGenerationModalOpen(false);
}
}
useEffect(() => {
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [handleKeyDown])
const discardAiWorkflow = () => {
if (backupWorkflow === null || backupWorkflow === undefined) {
toast("No backup workflow to discard to.");
return;
}
// Deep copy to avoid reference issues and reset state
const restored = JSON.parse(JSON.stringify(backupWorkflow));
setWorkflow(restored);
setBackupWorkflow(null);
setWorkflowDescription("");
setIsAiEditing(false); // Reset loading state
setWorkflowGenerationModalOpen(false);
toast.success("Changes discarded and previous workflow restored.");
};
const editAIWorkflow = () => {
setIsAiEditing(true);
setBackupWorkflow(null);
var envToSend = isCloud ? "Cloud" : "Shuffle"
for (var actionkey in workflow?.actions) {
envToSend = workflow?.actions[actionkey]?.environment
break
}
const data = {
query: workflowDescription,
workflow_id: workflow?.id,
environment: envToSend,
}
fetch(`${globalUrl}/api/v2/workflows/edit/llm`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
return response.json().then((json) => {
if (response.status !== 200) {
toast.error(json.reason || "Unexpected response. Please contact support@shuffler.io if this persists.", {
autoClose: 10000,
onClick: () => {
window.open("/docs/AI#self-hosting-models", "_blank")
}
})
setIsAiEditing(false);
return null;
}
if (json.success === true && typeof json.message === "string") {
toast(json.message);
setIsAiEditing(false);
return null;
}
if (json.success === false) {
toast(json.message || "Operation failed");
setIsAiEditing(false);
return null;
}
if (!json || Object.keys(json).length === 0) {
toast("AI edit failed: empty response");
setIsAiEditing(false);
return null;
}
toast.success("Workflow load done. Choose what to do.");
setBackupWorkflow(JSON.parse(JSON.stringify(workflow)));
setWorkflow(json);
setIsAiEditing(false);
return json;
});
})
.catch((error) => {
console.error("AI Workflow Edit Error:", error);
toast.error(`Failed to load LLM response due to: ${error.message || error}`);
setIsAiEditing(false);
});
};
const handleEdit = () => {
if (workflowDescription.trim() === "") {
return
}
editAIWorkflow()
};
const handleDiscard = () => {
setWorkflowDescription("");
}
const handleKeep = () => {
// Keep the AI-provided workflow: do not restore the backup.
// Clear the stored backup and reset modal state.
saveWorkflow(workflow);
setBackupWorkflow(null)
setWorkflowDescription("")
setIsAiEditing(false)
setWorkflowGenerationModalOpen(false);
};
if (open === false) {
if (hasBackup) {
}
return null
}
return (
<div style={{position: "fixed", bottom: 100, left: "40%", minWidth: 540, border: "1px solid rgba(255,255,255,0.3)", padding: 10, borderRadius: theme?.palette?.borderRadius || 8, background: currentTheme?.palette?.background?.default || "#222", }}>
{hasBackup && !isAiEditing ?
<ButtonGroup fullWidth style={{MarginBottom: 5, }}>
<Button
variant="outlined"
color="secondary"
size="small"
id="discard-changes-button"
onClick={discardAiWorkflow}
>
Discard (Ctrl+z)
</Button>
<Button
variant="aiButtonGhost"
size="small"
onClick={handleKeep}
id="keep-changes-button"
>
Keep (Ctrl+enter)
</Button>
</ButtonGroup>
:
<TextField
placeholder={`Describe how you want to edit your workflow here...`}
multiline
minRows={1}
value={workflowDescription}
onChange={(e) => setWorkflowDescription(e.target.value)}
disabled={isAiEditing}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
fullWidth
InputProps={{
endAdornment: (
<Button
id="try-it-button"
color="primary"
size="small"
variant="aiButton"
disabled={isAiEditing || workflowDescription.trim() === ""}
onClick={handleEdit}
style={{maxHeight: 40, minHeight: 40, whiteSpace: 'nowrap'}}
>
{isAiEditing
? <CircularProgress size={16} />
: <><SendIcon style={{ marginRight: 5, }} /> Ctrl+enter</>
}
</Button>
)
}}
/>
}
<Typography variant="body2" style={{ fontSize: 10, textAlign: "center", color: currentTheme.palette.text.secondary || "#ccc", marginTop: 10 }}>
AI Edits require you to manually review and accept changes.<br/> You can discard unwanted edits. Uses your configured LLM or shuffler.io AI credits. <b>Alpha</b> feature.
</Typography>
</div>
);
};
export default WorkflowGenerationModal;
+10 -10
View File
@@ -53,14 +53,14 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
: selectedOrganization.sso_config.SSORequired);
const [ssoCertificate, setSsoCertificate] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_certificate === undefined ||
selectedOrganization.sso_config.sso_certificate.length === 0
? ""
: selectedOrganization.sso_config.sso_certificate
);
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_certificate &&
selectedOrganization.sso_config.sso_certificate.length > 0
? selectedOrganization.sso_config.sso_certificate
: selectedOrganization.sso_config.sso_long_certificate || ""
);
const [openidClientId, setOpenidClientId] = React.useState(
selectedOrganization.sso_config === undefined
? ""
@@ -115,8 +115,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
setOpenidToken(selectedOrganization?.sso_config?.openid_token)
}
if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate) {
setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate)
if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate || ssoCertificate !== selectedOrganization?.sso_config?.sso_long_certificate) {
setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate || selectedOrganization?.sso_config?.sso_long_certificate)
}
if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) {
+10 -10
View File
@@ -392,8 +392,8 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: ".success-highlight",
css: {
"background-color": "#41dcab",
"border-color": "#41dcab",
"background-color": "#02CB70",
"border-color": "#02CB70",
"border-width": "5px",
"transition-property": "background-color",
"transition-duration": "0.5s",
@@ -412,8 +412,8 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: ".failure-highlight",
css: {
"background-color": "#8e3530",
"border-color": "#8e3530",
"background-color": "#F53434",
"border-color": "#F53434",
"border-width": "5px",
"transition-property": "background-color",
"transition-duration": "0.5s",
@@ -433,7 +433,7 @@ export default function defaultCytoscapeStyle(theme) {
selector: ".executing-highlight",
css: {
//"background-color": "#ffef47",
"border-color": "#ffef47",
"border-color": "#FECC00",
"border-width": "8px",
"transition-property": "border-width",
"transition-duration": "0.25s",
@@ -475,8 +475,8 @@ export default function defaultCytoscapeStyle(theme) {
selector: "edge.executing-highlight",
css: {
width: "5px",
"target-arrow-color": "#ffef47",
"line-color": "#ffef47",
"target-arrow-color": "#FECC00",
"line-color": "#FECC00",
"transition-property": "line-color, width",
"transition-duration": "0.25s",
},
@@ -495,9 +495,9 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: "edge.success-highlight",
css: {
width: "3px",
"target-arrow-color": "#41dcab",
"line-color": "#41dcab",
width: "4px",
"target-arrow-color": "#02CB70",
"line-color": "#02CB70",
"transition-property": "line-color, width",
"transition-duration": "0.5s",
"line-fill": "linear-gradient",
+82 -10
View File
@@ -391,6 +391,78 @@ export const getTheme = (themeMode, brandColor) =>
},
},
},
{
props: { variant: 'aiButton' },
style: {
background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
color: '#ffffff',
borderRadius: '4px',
whiteSpace: "nowrap",
textWrap: "normal",
border: 'none',
boxShadow: 'none',
transition: 'all 0.2s ease-in-out',
'&:hover': {
color: '#ffffff',
textShadow: '0 0 1px currentColor',
},
'&:active': {
background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
},
'&:disabled': {
background: themeMode === "dark" ? '#494949' : '#C9C9C9',
color: themeMode === "dark" ? '#9E9E9E' : '#616161',
},
},
},
{
props: { variant: 'aiButtonGhost' },
style: {
background: 'transparent',
border: 'none',
borderRadius: '4px',
color: '#ffffff',
whiteSpace: "nowrap",
textWrap: "normal",
boxShadow: 'none',
transition: 'all 0.2s ease-in-out',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: '4px',
padding: '2px',
background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
maskComposite: 'exclude',
WebkitMaskComposite: 'xor',
transition: 'opacity 0.2s ease-in-out',
zIndex: -1,
},
'&:hover': {
background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
color: '#ffffff',
'&::before': {
opacity: 0,
},
},
'&:active': {
background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
},
'&:disabled': {
background: 'transparent',
color: themeMode === "dark" ? '#9E9E9E' : '#616161',
'&::before': {
background: themeMode === "dark" ? '#494949' : '#C9C9C9',
},
},
},
},
],
},
MuiTab: {
@@ -412,39 +484,39 @@ export const getTheme = (themeMode, brandColor) =>
MuiCssBaseline: {
styleOverrides: `
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light');
src: local('Inter Light'), local('Inter-Light');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular');
src: local('Inter Regular'), local('Inter-Regular');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium');
src: local('Inter Medium'), local('Inter-Medium');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
src: local('Roboto SemiBold'), local('Roboto-SemiBold');
src: local('Inter SemiBold'), local('Inter-SemiBold');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold');
src: local('Inter Bold'), local('Inter-Bold');
}
`,
},
+4
View File
@@ -87,6 +87,10 @@ const Admin2 = (props) => {
leads.push("distribution partner");
}
if (responseJson.lead_info.channel_partner) {
leads.push("channel partner");
}
if (responseJson.lead_info.service_partner) {
leads.push("service partner");
}
+219 -43
View File
@@ -1,11 +1,14 @@
import React, { useState, useEffect, useContext, memo } from "react";
import { Context } from "../context/ContextApi.jsx";
import { useNavigate, Link, useLocation } from "react-router-dom";
import { getTheme } from "../theme.jsx";
import { toast } from "react-toastify"
import ReactJson from "react-json-view-ssr";
import { v4 as uuidv4} from "uuid";
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx";
import {
Box,
Button,
ButtonGroup,
Typography,
@@ -13,6 +16,7 @@ import {
CircularProgress,
Tooltip,
IconButton,
TextField,
} from '@mui/material'
import {
@@ -21,6 +25,8 @@ import {
RestartAlt as RestartAltIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Send as SendIcon,
Error as ErrorIcon,
} from '@mui/icons-material'
import {
@@ -33,15 +39,19 @@ const AgentUI = (props) => {
const [buttonState, setButtonState] = useState("timeline")
const [execution, setExecution] = useState(null)
const [agentActionResult, setAgentActionResult] = useState(null)
const [agentRequestLoading, setAgentRequestLoading] = useState(false)
const [data, setData] = useState({})
const [openIndexes, setOpenIndexes] = useState([])
const [disableButtons, setDisableButtons] = useState(false)
const [originalStartTime, setOriginalStartTime] = useState(0)
const [latestEndTime, setLatestEndTime] = useState(0)
const [showAgentStarter, setShowAgentStarter] = useState(false)
const [actionInput, setActionInput] = useState("")
const {themeMode} = useContext(Context)
const theme = getTheme(themeMode)
const navigate = useNavigate();
const agentWrapperStyle = {
width: 1000,
@@ -64,6 +74,10 @@ const AgentUI = (props) => {
return
}
if (node_id === undefined || node_id === null || node_id === "") {
return
}
var found = false
for (var key in execution_data.results) {
const item = execution_data.results[key]
@@ -86,6 +100,16 @@ const AgentUI = (props) => {
if (found === false) {
toast.warn("Failed to find the relevant AI Agent result")
if (execution_data?.results?.length === 1) {
setAgentActionResult(execution_data.results[0])
const validatedData = validateJson(execution_data.results[0].result)
if (validatedData.valid) {
setData(validatedData.result)
} else {
toast.warn("Action output result is not valid JSON!")
}
}
}
}
@@ -95,10 +119,10 @@ const AgentUI = (props) => {
return
}
if (node_id === undefined || node_id === null) {
toast.error("No node ID provided. Please provide node_id in the URL.")
return
}
//if (node_id === undefined || node_id === null || node_id === "") {
// toast.error("No node ID provided. Please provide node_id in the URL.")
// return
//}
if (authorization === undefined || authorization === null) {
toast.error("No authorization provided. Please provide authorization in the URL.")
@@ -166,6 +190,7 @@ const AgentUI = (props) => {
const url = `${globalUrl}/api/v1/apps/agent/run?rerun=true&decision_id=${decision?.run_details?.id}`
var body = agentActionResult.action
console.log("BODY: ", body)
body.source_execution = execution.execution_id
body.source_workflow = execution.workflow.id
@@ -202,10 +227,11 @@ const AgentUI = (props) => {
const executionId = params.get("execution_id")
const nodeId = params.get("node_id")
const authorization = params.get("authorization")
if (executionId !== undefined && executionId !== null && nodeId !== undefined && nodeId !== null && authorization !== undefined && authorization !== null) {
if (executionId !== undefined && executionId !== null && authorization !== undefined && authorization !== null) {
GetExecution(executionId, nodeId, authorization)
} else {
toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
setShowAgentStarter(true)
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
}
}, [])
@@ -221,6 +247,11 @@ const AgentUI = (props) => {
<Tooltip title="Finished" placement="top">
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
</Tooltip>
:
item.status === "ABORTED" || item.status === "FAILURE" ?
<Tooltip title={`${item.status}: Check the raw data`} placement="top">
<ErrorIcon style={{color: red, marginRight: 10, }} />
</Tooltip>
:
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
<HourglassDisabledIcon style={{marginRight: 10, }} />
@@ -246,11 +277,13 @@ const AgentUI = (props) => {
const validate = validateJson(item.details)
const itemStartTime = item.start_time
var itemEndTime = item.end_time
if (itemStartTime !== undefined && (itemStartTime < originalStartTime || originalStartTime === 0)) {
setOriginalStartTime(itemStartTime)
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
console.log("Rerender 1")
//setOriginalStartTime(itemStartTime)
}
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
console.log("Rerender 2")
setLatestEndTime(itemEndTime)
}
@@ -280,23 +313,47 @@ const AgentUI = (props) => {
borderTop: "1px solid " + theme.palette.surfaceColor,
borderRadius: theme.palette.borderRadius,
}}
onMouseEnter={() => {
if (!hovered) {
console.log("HOVER")
setHovered(true)
}
}}
onMouseLeave={() => {
if (hovered) {
setHovered(false)
}
}}
>
<div
style={{
display: "flex",
backgroundColor: hovered ? theme.palette.surfaceColor : "inherit",
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={(e) => {
if (item.details === undefined || item.details === null || item.details === "") {
toast("No details to open")
if (item?.category === "agent" && item?.type === "agent") {
// Show all the data
if (openIndexes.includes(index)) {
console.log("Rerender 3")
setOpenIndexes(openIndexes.filter((i) => i !== index))
} else {
console.log("Rerender 4")
setOpenIndexes([...openIndexes, index])
}
} else {
toast.warn("No details to open")
}
return
}
if (openIndexes.includes(index)) {
console.log("Rerender 5")
setOpenIndexes(openIndexes.filter((i) => i !== index))
} else {
console.log("Rerender 6")
setOpenIndexes([...openIndexes, index])
}
}}
@@ -421,18 +478,31 @@ const AgentUI = (props) => {
const TimelineRender = (props) => {
const { agent_data } = props;
const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
var timelineItems = [
{
"label": "AI Agent 2",
"type": "agent",
"category": "agent",
"details": actionResult?.result,
"status": agent_data.status,
"start_time": agent_data.started_at,
"end_time": agent_data.completed_at,
"status": agent_data?.status,
"start_time": agent_data?.started_at,
"end_time": agent_data?.completed_at,
},
]
// Autofixer for result lol
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
const verifiedInput = validateJson(actionResult?.result)
if (verifiedInput.valid === true && verifiedInput.result?.decisions !== undefined && verifiedInput.result?.decisions !== null) {
agent_data.decisions = verifiedInput.result?.decisions
setAgentActionResult(actionResult)
}
}
var sortedTimelineItems = []
for (var key in agent_data?.decisions) {
const item = agent_data.decisions[key]
@@ -501,39 +571,145 @@ const AgentUI = (props) => {
)
}
const submitInput = (inputText) => {
//toast.info("Submitting AI Agent input: " + inputText);
setAgentRequestLoading(true)
//setShowAgentStarter(false);
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
if (inputText === undefined || inputText === null || inputText === "") {
toast.error("Please provide a valid input for the AI Agent.")
return
}
// 1. Run the execution. Can this be a single-action run?
// 2. Get the execution ID and node ID from the response.
const uuid = uuidv4()
const data = {
"id": uuid,
"name":"agent",
//"app_name":"Shuffle AI",
"app_name":"AI Agent", // Failover for rerun
"app_id":"shuffle_agent",
"app_version":"1.0.0",
"environment":"cloud",
"parameters":[
{
"name":"app_name",
"value":"openai"
},
{
"name":"input",
"value": inputText
},
{
"name":"action",
"value":"list_tickets"
}
]}
const url = `${globalUrl}/api/v1/apps/agent_starter/run`
fetch(url, {
method: "POST",
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
setAgentRequestLoading(false)
return response.json()
})
.then((responseJson) => {
//toast.success("Got response!")
console.log("Agent run response: ", responseJson)
if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) {
navigate("?execution_id=" + responseJson.execution_id + "&authorization=" + responseJson.authorization)
setShowAgentStarter(false)
GetExecution(responseJson.execution_id, "", responseJson.authorization)
}
})
.catch((error) => {
setAgentRequestLoading(false)
toast.error("Error: " + error)
})
}
return (
<div style={agentWrapperStyle}>
{/*
<Typography variant="h4">
Agent Input: {data.input}
</Typography>
*/}
<ButtonGroup style={{marginTop: 50, }}>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("default");
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
}}
>
Timeline
</Button>
</ButtonGroup>
{showAgentStarter ?
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}>
<img src="/images/logos/agent.svg" style={{
width: 200,
height: 200,
}} />
<div />
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
<Typography variant="h5" style={{marginTop: 30, }}>
Shuffle AI Agents
</Typography>
<TextField
label="What do you want to do?"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline
minRows={2}
defaultValue={execution?.execution_id || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<SendIcon
color="primary"
/>
</IconButton>
</Tooltip>
),
}}
/>
</Box>
:
<div>
<ButtonGroup style={{marginTop: 50, }}>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("default");
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
}}
>
Timeline
</Button>
</ButtonGroup>
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
}
</div>
}
</div>
)
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -54,7 +54,7 @@ import YAML from "yaml";
import { MuiChipsInput } from "mui-chips-input";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import words from "shellwords";
import { split } from "shellwords";
import AvatarEditor from "react-avatar-editor";
import { Context } from "../context/ContextApi.jsx";
@@ -119,7 +119,7 @@ const parseCurl = (s) => {
}
try {
var args = rewrite(words.split(s));
var args = rewrite(split(s));
} catch (e) {
return s;
}
+248 -15
View File
@@ -233,6 +233,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const [secondaryApp, setSecondaryApp] = useState({});
const [firstRequest, setFirstRequest] = useState(true);
const [publishModalOpen, setPublishModalOpen] = React.useState(false);
const [showDistributionPopup, setShowDistributionPopup] = React.useState(false);
const [categories, setCategories] = useState(appCategories)
const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]);
@@ -743,7 +744,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}
};
const activateApp = (action) => {
const activateApp = (action, org_id, multiple_request = false) => {
if (serverside === true) {
return
}
@@ -753,17 +754,20 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (action !== undefined && action !== null) {
url = `${globalUrl}/api/v1/apps/${appId}/${action}`
}
fetch(url, {
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"Accept": "application/json",
"Org-Id": org_id !== undefined && org_id !== null && org_id?.length > 0 ? org_id : userdata.active_org.id
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Failed to activate");
console.log("Failed to activate: " + response.statusText);
}
return response.json();
@@ -772,34 +776,37 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (responseJson.success === false) {
if (action === undefined || action === null) {
if (responseJson.reason !== undefined) {
toast("Failed to activate the app: "+responseJson.reason);
toast.warn("Failed to activate the app: "+responseJson.reason);
} else {
toast("Failed to activate the app");
toast.warn("Failed to activate the app");
}
} else {
if (responseJson.reason !== undefined) {
toast("Failed to perform action: "+responseJson.reason);
toast.warn("Failed to perform action: "+responseJson.reason);
} else {
toast(`Failed to perform action. Please try again or contact ${supportEmail}`);
toast.warn(`Failed to perform action. Please try again or contact ${supportEmail}`);
}
}
} else {
if (checkLogin !== undefined && checkLogin !== null) {
if (!showDistributionPopup && (checkLogin !== undefined && checkLogin !== null) && !multiple_request) {
checkLogin()
}
if (action === undefined || action === null) {
if (appExists) {
toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
toast.success("App deactivated for your organization! Existing workflows with the app will continue to work.")
} else {
toast("App activated for your organization!")
toast.success("App activated for your organization!")
}
} else {
if (responseJson.success && !multiple_request &&(responseJson.reason !== undefined || responseJson.reason !== null)) {
toast.success(`${responseJson.reason}`);
}
}
}
})
.catch((error) => {
toast(error.toString());
toast.error(error.toString());
});
};
@@ -1443,7 +1450,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const index = searchClient.initIndex("appsearch");
console.log("Running appsearch for: ", appname);
if (appname === "integration") {
// Redirect to https://singul.io
window.location.href = "https://singul.io"
} else if (appname === "shuffle_agent") {
navigate("/agents")
}
index
.search(appname)
@@ -3909,6 +3921,220 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
</Dialog>
) : null;
const handleActivateApp = (id, action) => {
if (action === "activate_all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
)
// run app activation request for each org
const orgIds = childOrgs.map((data) => data.id);
orgIds.forEach((orgId) => {
activateApp("activate", orgId, true)
})
setTimeout(() => {
toast.success("App activated for all sub-orgs");
}, 5000)
} else if (action === "deactivate_all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
)
const orgIds = childOrgs.map((data) => data.id);
// run app deactivation request for each org
orgIds.forEach((orgId) => {
activateApp("deactivate", orgId, true)
});
setTimeout(() => {
toast.success("App deactivated for all sub-orgs");
}, 5000);
} else if (action === "activate_single") {
if (id === null) {
toast.error("Please select a sub-org to activate the app for.");
return;
}
activateApp("activate", id);
} else if (action === "deactivate_single") {
if (id === null) {
toast.error("Please select a sub-org to deactivate the app for.");
return;
}
activateApp("deactivate", id);
}
};
const appDistributinModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => setShowDistributionPopup(false)}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius || 3,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: 600,
minHeight: 320,
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
p: 2,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pr: 1,
pb: 1,
}}
style={{
padding: "50px 50px 50px 50px",
}}
>
<Typography variant="h6" fontWeight={600} color="text.primary">
Suborg App Distribution
</Typography>
<IconButton
onClick={() => setShowDistributionPopup(false)}
sx={{
color: theme.palette.text.primary,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent
sx={{
color: "rgba(255,255,255,0.85)",
px: 2,
pt: 1,
pb: 2,
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<MenuItem
value="none"
onClick={() => handleActivateApp(null, "deactivate_all")}
sx={{
borderRadius: 1,
px: 2,
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.08)',
},
}}
>
Deactivate for all suborgs
</MenuItem>
<MenuItem
value="all"
onClick={() => handleActivateApp(null, "activate_all")}
sx={{
borderRadius: 1,
px: 2,
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.08)',
},
}}
>
Activate for all suborgs
</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) return null;
const imageSize = 28;
const imageStyle = {
width: imageSize,
height: imageSize,
borderRadius: '50%',
objectFit: 'cover',
marginRight: 12,
};
const imageSrc = data.image || theme.palette.defaultImage;
return (
<MenuItem
key={index}
value={data.id}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 1,
px: 2,
py: 1,
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.06)',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<img alt={data.name} src={imageSrc} style={imageStyle} />
<Typography variant="body1" color="text.primary">
{data.name}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
color="secondary"
size="small"
onClick={() => handleActivateApp(data.id, "activate_single")}
sx={{
textTransform: 'none',
borderRadius: '6px',
fontWeight: 500,
}}
>
Activate
</Button>
<Button
variant="outlined"
color="secondary"
size="small"
onClick={() => handleActivateApp(data.id, "deactivate_single")}
sx={{
textTransform: 'none',
borderRadius: '6px',
fontWeight: 500,
}}
>
Deactivate
</Button>
</Box>
</MenuItem>
);
})}
</DialogContent>
</Dialog>
) : null;
const landingpageDataBrowser = (
<div
style={{
@@ -3918,6 +4144,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}}
>
{publishModal}
{appDistributinModal}
<div style={{ display: "flex", position: "relative" }}>
{isMobile ? null : (
<Breadcrumbs
@@ -4066,7 +4293,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
</Button>
}
{isMobile || app?.reference_org === userdata?.active_org?.id ? null : (
{isMobile || app?.reference_org === userdata?.active_org?.id || (app?.suborg_distribution?.includes(userdata?.active_org?.id)) ? null : (
<Button
variant={userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ? "outlined": "contained"}
component="label"
@@ -4168,7 +4395,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
Try the API
</Button>
</a>
<Select
{app?.reference_org === userdata?.active_org?.id ? (
<Select
value={sharingConfiguration}
disabled={!isCloud}
onChange={(event) => {
@@ -4223,6 +4451,11 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
);
})}
</Select>
): null}
{userdata && (userdata?.active_org?.creator_org?.length > 0 || userdata?.active_org?.child_orgs?.length === 0) ? null : (
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}} >Distribute App</Button>
)}
</div>
) : (
<a
+16 -12
View File
@@ -949,29 +949,33 @@ const CustomLabelDropdown = connectRefinementList(LabelDropdown);
const filterApps = (apps, searchQuery, selectedCategory, selectedLabel) => {
if (!Array.isArray(apps)) return [];
const normalizedSearchQuery = (searchQuery || "").toLowerCase();
return apps.filter((app) => {
if (!app) return false;
const matchesSearchQuery = (
searchQuery === "" || // If searchQuery is empty, match all apps
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(app.tags && app.tags.some(tag =>
tag.toLowerCase().includes(searchQuery.toLowerCase())
normalizedSearchQuery === "" || // If searchQuery is empty, match all apps
(app.name && app.name.toLowerCase().includes(normalizedSearchQuery)) ||
(app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
tag && typeof tag === 'string' && tag.toLowerCase().includes(normalizedSearchQuery)
)) ||
(app.categories && app.categories.some((category) =>
category.toLowerCase().includes(searchQuery.toLowerCase())
(app.categories && Array.isArray(app.categories) && app.categories.some((category) =>
category && typeof category === 'string' && category.toLowerCase().includes(normalizedSearchQuery)
))
);
const matchesSelectedCategories = (
selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && app.categories.some(category =>
selectedCategory.includes(category)
!Array.isArray(selectedCategory) || selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && Array.isArray(app.categories) && app.categories.some(category =>
category && selectedCategory.includes(category)
))
);
const matchesSelectedTags = (
selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && app.tags.some(tag =>
selectedLabel.includes(tag)
!Array.isArray(selectedLabel) || selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
tag && selectedLabel.includes(tag)
))
);
+10 -16
View File
@@ -315,7 +315,7 @@ const RadialChart = ({keys, setSelectedCategory}) => {
// What data do we fill in here? Idk
const Dashboard = (props) => {
const { globalUrl, isLoggedIn } = props;
const { globalUrl, userdata, isLoggedIn } = props;
//const alert = useAlert();
const [bigChartData, setBgChartData] = useState("data1");
const [dayAmount, setDayAmount] = useState(7);
@@ -323,10 +323,10 @@ const Dashboard = (props) => {
const [stats, setStats] = useState({});
const [changeme, setChangeme] = useState("");
const [statsRan, setStatsRan] = useState(false);
const [keys, setKeys] = useState([])
const [treeKeys, setTreeKeys] = useState([])
const [keys, setKeys] = useState([])
const [treeKeys, setTreeKeys] = useState([])
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("");
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("")
const [selectedUsecases, setSelectedUsecases] = useState([]);
const [usecases, setUsecases] = useState([]);
const [workflows, setWorkflows] = useState([]);
@@ -750,8 +750,6 @@ const Dashboard = (props) => {
const newname = data.key !== undefined ? data.key.replaceAll("_", " ") : ""
console.log("KEYDATA: ", data)
const loadNewStats = (newkey) => {
const resp = LoadStats(globalUrl, newkey)
if (resp !== undefined) {
@@ -807,7 +805,7 @@ const Dashboard = (props) => {
backgroundColor: theme.palette.inputColor,
color: "white",
height: 40,
maxWidth: 150,
maxWidth: 200,
borderRadius: theme.palette?.borderRadius,
}}
>
@@ -829,10 +827,12 @@ const Dashboard = (props) => {
</Select>
}
</div>
<DashboardBarchart
timelineData={data}
height={50}
/>
</Paper>
</Draggable>
)
@@ -848,14 +848,6 @@ const Dashboard = (props) => {
: null}
</div>
{/*widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
<Draggable>
<Paper style={{height: 350, width: 500, padding: "15px 15px 15px 15px", }}>
<LineChartWrapper keys={widgetData[0]} height={280} width={470} />
</Paper>
</Draggable>
*/}
{newWidgetData === undefined || newWidgetData === null || newWidgetData === [] ? null :
newWidgetData.map((data, index) => {
@@ -872,7 +864,9 @@ const Dashboard = (props) => {
);
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto", paddingTop: 10, }}>{data}</div>
<div style={{ maxWidth: 1366, margin: "auto", paddingTop: 10, }}>
{data}
</div>
);
return dataWrapper;
+17 -6
View File
@@ -113,19 +113,29 @@ export const CopyToClipboard = (props) => {
}
export const Paragraph = (props) => {
// Filter out stray HTML artifacts like '>' or '/>' caused by HTML parsing edge-cases
const cleanedChildren = React.Children.toArray(props.children).filter((child) => {
if (typeof child === 'string') {
const trimmed = child.trim();
// Remove single '>' or '/>' leftovers
if (trimmed === '>' || trimmed === '/>') return false;
}
return true;
});
const element = React.createElement(
`p`,
{},
props.children,
cleanedChildren,
)
if (props.children[0] != undefined) {
if(typeof props.children[0] === "string") {
if (props.children[0].includes('.mp4')) {
if (cleanedChildren[0] !== undefined) {
if(typeof cleanedChildren[0] === "string") {
if (cleanedChildren[0].includes('.mp4')) {
return (
<div>
<video width="640" height="480" controls>
<source src={`${props.children[0]}`} type="video/mp4" />
<source src={`${cleanedChildren[0]}`} type="video/mp4" />
</video>
</div>
)
@@ -169,6 +179,7 @@ export const Img = (props) => {
// Find parent container and check width
const isArticlePage = window.location.pathname.includes("/articles/")
const isFormPage = window.location.pathname.includes("/forms/")
const isWorkflowPage = window.location.pathname.includes("/workflows/")
var height = "auto"
var width = isArticlePage ? 1000 : isFormPage ? 400: 750
@@ -176,7 +187,7 @@ export const Img = (props) => {
const theme = getTheme(themeMode)
const docsImageStyle = {
border: isFormPage ? null : "1px solid rgba(255,255,255,0.3)",
border: isFormPage || isWorkflowPage ? null : "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette?.borderRadius,
width: width,
maxWidth: width,
+84 -6
View File
@@ -5,6 +5,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { isMobile } from "react-device-detect";
import { toast } from 'react-toastify';
import { useInterval } from "react-powerhooks";
import ReactGA from 'react-ga4';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import {
@@ -157,7 +158,7 @@ const FreePlanCard = ({ classes }) => {
);
};
const MarketplaceCard = ({ classes }) => {
const MarketplaceCard = ({ classes, isCloud }) => {
const marketplaceOptions = [
{
name: "Open Source Install",
@@ -216,6 +217,15 @@ const MarketplaceCard = ({ classes }) => {
>
<button
onClick={() => {
// Track marketplace click
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_marketplace_option",
label: option.name,
});
}
if (option.valid === true) {
window.open(option.link, "_blank")
}
@@ -443,7 +453,7 @@ const LoginPage = props => {
return (username.length > 0 && password.length > 0);
}
return (username.length > 1 && password.length > 8);
return (username.length > 1 && password.length > 9);
}
if (isLoggedIn === true && serverside !== true) {
@@ -512,6 +522,15 @@ const LoginPage = props => {
const onSubmit = (e) => {
//toast("Testing from login page")
// Track form submission attempt
if (isCloud) {
ReactGA.event({
category: "authentication",
action: register ? "submit_login_form" : "submit_register_form",
label: register ? (loginWithSSO ? "SSO Login Form" : "Email Login Form") : "Email Registration Form",
});
}
setMessage("")
setLoginLoading(true)
e.preventDefault()
@@ -556,6 +575,14 @@ const LoginPage = props => {
setLoginLoading(false)
if (responseJson["success"] === false) {
// Track failed login event
if (isCloud) {
ReactGA.event({
category: "authentication",
action: loginWithSSO ? "login_sso_failed" : "login_failed",
label: loginWithSSO ? "SSO Login Failed" : "Email Login Failed",
});
}
setLoginInfo(responseJson["reason"])
} else {
if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") {
@@ -583,6 +610,14 @@ const LoginPage = props => {
return
}
// Track successful login event
if (isCloud) {
ReactGA.event({
category: "authentication",
action: loginWithSSO ? "login_sso_success" : "login_success",
label: loginWithSSO ? "SSO Login" : "Email Login",
});
}
setLoginInfo("Successful login! Redirecting you in 3 seconds...")
for (var key in responseJson["cookies"]) {
@@ -643,6 +678,14 @@ const LoginPage = props => {
.then(response =>
response.json().then(responseJson => {
if (responseJson["success"] === false) {
// Track failed registration event
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "register_failed",
label: "Email Registration Failed",
});
}
setLoginInfo(responseJson["reason"])
} else {
if (responseJson["reason"] === "shuffle_account") {
@@ -659,6 +702,16 @@ const LoginPage = props => {
}
setLoginLoading(false)
// Track successful registration event
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "register_success",
label: "Email Registration",
});
}
setLoginInfo("Successful registration! Redirecting in 3 seconds...")
@@ -707,15 +760,40 @@ const LoginPage = props => {
}
const HandleLoginWithSSO = () => {
// Track SSO login attempt
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_sso_login",
label: "SSO Login Attempt",
});
}
setPassword("")
setLoginInfo("")
setLoginWithSSO(true)
}
var formtitle = register ? <div>Welcome Back!</div> : <div>Create your account</div>
var formButton = !isCloud ? "" : register ? <div style={{ display: "flex" }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Dont have an account yet?</div> <Link style={hrefStyle} to={`/register${parsedsearch}`}><div>Register here</div></Link></div> : <>
var formButton = !isCloud ? "" : register ? <div style={{ display: "flex" }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Dont have an account yet?</div> <Link style={hrefStyle} to={`/register${parsedsearch}`} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_register_link",
label: "Switch to Register",
});
}
}}><div>Register here</div></Link></div> : <>
<div style={{ display: "flex", marginTop: 40, marginBottom: -10 }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Already have an account?</div> <Link style={hrefStyle} to={`/login${parsedsearch}`}><div>Login here</div></Link></div>
<div style={{ display: "flex", marginTop: 40, marginBottom: -10 }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Already have an account?</div> <Link style={hrefStyle} to={`/login${parsedsearch}`} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_login_link",
label: "Switch to Login",
});
}
}}><div>Login here</div></Link></div>
</>
//<Link to={`/login${parsedsearch}`} style={hrefStyle}><div>Click here to Login</div></Link>
@@ -854,7 +932,7 @@ const LoginPage = props => {
helperText={
handleValidateForm(username, password)
? ""
: "Password must be at least 9 characters long"
: "Password must be at least 10 characters long"
}
/>
</div>
@@ -1025,7 +1103,7 @@ const LoginPage = props => {
<div className={classes.divider}>
<span>OR</span>
</div>
<MarketplaceCard classes={classes} />
<MarketplaceCard classes={classes} isCloud={isCloud} />
</>
)}
</div>
+1 -1
View File
@@ -344,7 +344,7 @@ const LoginDialog = (props) => {
variant="body2"
style={{ marginBottom: 20, color: "white" }}
>
<b>1.</b> Make sure shuffle-database folder has correct access, and that you have a minimum of <b>2Gb of RAM available</b>:{" "}
<b>1.</b> Make sure shuffle-database folder has correct access, and that you have a minimum of <b>4Gb of RAM available</b>:{" "}
<br />
<br />
sudo chown -R 1000:1000 shuffle-database
+3 -1
View File
@@ -13,7 +13,7 @@ import { makeStyles } from '@mui/material/styles';
import { useInterval } from "react-powerhooks";
import { isMobile } from "react-device-detect";
import Markdown from "react-markdown";
import theme from '../theme.jsx';
import {getTheme} from '../theme.jsx';
import rehypeRaw from "rehype-raw";
import RecentWorkflow from "../components/RecentWorkflow.jsx";
@@ -58,6 +58,8 @@ const hrefStyle = {
const RunWorkflow = (defaultprops) => {
const { globalUrl, userdata, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, register, serverside } = defaultprops;
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const { supportEmail } = useContext(Context);
let navigate = useNavigate();
+1 -1
View File
@@ -558,7 +558,7 @@ export const handleReactJsonClipboard = (copy) => {
document.execCommand("copy");
console.log("COPYING!");
toast("Copied value to clipboard, NOT json path.")
toast.success("Copied Value, NOT json path.")
} else {
console.log("Failed to copy from " + elementName + ": ", copyText);
}
+626 -50
View File
@@ -17,11 +17,14 @@ import GetAppIcon from '@mui/icons-material/GetApp';
// Material UI & Components
import { makeStyles } from "@mui/styles";
import { Navigate } from "react-router-dom";
import { isMobile } from "react-device-detect"
import ReactGA from 'react-ga4';
import LineChartWrapper from "../components/LineChartWrapper.jsx";
import SecurityFramework from '../components/SecurityFramework.jsx';
import EditWorkflow from "../components/EditWorkflow.jsx"
import Priority from "../components/Priority.jsx";
import { Context } from "../context/ContextApi.jsx";
import { isMobile } from "react-device-detect"
// Material UI Components
import {
@@ -99,6 +102,9 @@ import {
Psychology as PsychologyIcon,
Wifi as WifiIcon,
Devices as DevicesIcon,
AutoAwesome as AutoAwesomeIcon,
BarChart as BarChartIcon,
Lock as LockIcon,
} from "@mui/icons-material";
// Additional Components
@@ -115,14 +121,26 @@ import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinem
import { debounce } from "lodash";
import { removeQuery } from "../components/ScrollToTop.jsx";
import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx"
import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/AngularWorkflow.jsx"
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e");
const svgSize = 24;
const imagesize = 22;
const imagesize = 23;
// Session-based modal visibility helper
const AI_ANNOUNCEMENT_SESSION_KEY = "ai_announcement_session";
const getCookie = (name) => {
if (typeof document === "undefined") return "";
const pattern = `; ${document.cookie}`;
const parts = pattern.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(";").shift() || "");
}
return "";
};
@@ -212,8 +230,14 @@ export const GetIconInfo = (action) => {
"release",
],
},
{
key: "secret",
values: [
"api",
"password",
"protect",
],
}
];
var selectedKey = ""
@@ -402,6 +426,12 @@ export const GetIconInfo = (action) => {
iconBackgroundColor: "green",
originalIcon: <DevicesIcon />,
},
secret: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <LockIcon />,
}
}
/*
@@ -670,6 +700,10 @@ const Workflows2 = (props) => {
const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false);
const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false);
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true" || isCloud)
const imgSize = 60;
const { themeMode, brandColor, brandName } = useContext(Context);
@@ -728,6 +762,8 @@ const Workflows2 = (props) => {
var upload = "";
const [workflows, setWorkflows] = React.useState([]);
const [workflowTimelines, setWorkflowTimelines] = React.useState([]);
const [backgroundWorkflows, setBackgroundWorkflows] = React.useState([]);
const [backupWorkflows, setBackupWorkflows] = React.useState([]);
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [selectedUsecases, setSelectedUsecases] = React.useState([]);
@@ -767,6 +803,8 @@ const Workflows2 = (props) => {
const [actionImageList, setActionImageList] = React.useState([{ "large_image": "" }])
const [firstLoad, setFirstLoad] = React.useState(true);
const [aiAnnouncementModalOpen, setAiAnnouncementModalOpen] = React.useState(false);
const sessionRef = useRef("");
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [usecases, setUsecases] = React.useState([]);
const [allUsecases, setAllUsecases] = React.useState({
@@ -875,10 +913,167 @@ const Workflows2 = (props) => {
}
//const isCloud =
// window.location.host === "localhost:3002" ||
// window.location.host === "shuffler.io";
const isCloud = false
React.useEffect(() => {
if (!isLoggedIn) return;
const bannerID = "banner_ai_announcement";
const cookieSession = getCookie("__session") || "";
sessionRef.current = cookieSession;
try {
const storedSession = localStorage.getItem(AI_ANNOUNCEMENT_SESSION_KEY) || "";
// If backend says it's dismissed, sync storage and exit
if (userdata && Array.isArray(userdata.tutorials)) {
const alreadyDismissed = userdata.tutorials.some((t) => t?.name === bannerID);
if (alreadyDismissed) {
if (cookieSession && storedSession !== cookieSession) {
localStorage.setItem(AI_ANNOUNCEMENT_SESSION_KEY, cookieSession);
}
return;
}
} else if (!userdata) {
// Wait for userdata to load
return;
}
// Open only if not dismissed in this session
if (cookieSession && storedSession !== cookieSession) {
setAiAnnouncementModalOpen(true);
}
} catch {
// If storage is unavailable, fallback to a single open per mount
setAiAnnouncementModalOpen((open) => open || true);
}
}, [isLoggedIn, userdata]);
const handleCloseAiAnnouncement = React.useCallback(() => {
try {
const cookieSession = sessionRef.current || getCookie("__session") || "";
if (cookieSession) {
localStorage.setItem(AI_ANNOUNCEMENT_SESSION_KEY, cookieSession);
}
} catch {
// ignore storage access issues
}
setAiAnnouncementModalOpen(false);
}, []);
const dismissAiAnnouncement = () => {
const bannerID = "banner_ai_announcement";
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "dismiss_announcement",
label: userdata?.active_org?.id || userdata?.id || "",
});
}
handleCloseAiAnnouncement();
// Open Create Workflow modal (EditWorkflow) and temporarily highlight inputs/buttons
try {
setModalOpen(true)
setIsEditing(false)
setNewWorkflowName("")
setNewWorkflowDescription("")
setDefaultReturnValue("")
setEditingWorkflow({})
setNewWorkflowTags([])
setSelectedUsecases([])
// Wait a moment for the drawer to mount, then highlight
setTimeout(() => {
const BORDER = '#4CAF50';
const DURATION = 1500;
const highlightTextField = (inputEl) => {
if (!inputEl) return;
const formControl = inputEl.closest('.MuiFormControl-root') || inputEl.closest('.MuiInputBase-root') || inputEl.parentElement;
const inputRoot = formControl?.querySelector('.MuiOutlinedInput-root') || formControl?.querySelector('.MuiInputBase-root') || formControl;
const notch = formControl?.querySelector('fieldset');
const prevBoxShadow = inputRoot?.style?.boxShadow;
const prevBorderColor = notch?.style?.borderColor;
const prevBorderWidth = notch?.style?.borderWidth;
if (inputRoot) inputRoot.style.boxShadow = '0 0 0 3px rgba(76,175,80,0.45)';
if (notch) {
notch.style.borderColor = BORDER;
notch.style.borderWidth = '2px';
}
setTimeout(() => {
try {
if (inputRoot) inputRoot.style.boxShadow = prevBoxShadow || '';
if (notch) {
notch.style.borderColor = prevBorderColor || '';
notch.style.borderWidth = prevBorderWidth || '';
}
} catch (_) {}
}, DURATION);
};
const highlightButton = (btnEl) => {
if (!btnEl) return;
const prevBoxShadow = btnEl.style.boxShadow;
const prevBorder = btnEl.style.border;
const prevRadius = btnEl.style.borderRadius;
btnEl.style.boxShadow = '0 0 0 3px rgba(76,175,80,0.45)';
btnEl.style.border = '2px solid ' + BORDER;
btnEl.style.borderRadius = '6px';
setTimeout(() => {
try {
btnEl.style.boxShadow = prevBoxShadow || '';
btnEl.style.border = prevBorder || '';
btnEl.style.borderRadius = prevRadius || '';
} catch (_) {}
}, DURATION);
};
const nameEl = document.getElementById('Enter-Workflow-Name');
highlightTextField(nameEl);
const descEl = document.getElementById('Workflow-Description');
highlightTextField(descEl);
const aiBtn = document.getElementById('ai-generate-button');
highlightButton(aiBtn);
}, 250);
} catch (e) {
console.debug('Failed to open and highlight Create Workflow modal:', e);
}
fetch(globalUrl + '/api/v1/users/updateuser', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
tutorial: bannerID,
user_id: userdata.id
}),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Failed to dismiss AI announcement banner");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success) {
console.log("AI announcement banner dismissed successfully");
}
})
.catch((error) => {
console.log("Error dismissing AI announcement:", error);
});
};
const findWorkflow = (filters) => {
console.log("Using filters: ", filters)
@@ -1154,6 +1349,199 @@ const Workflows2 = (props) => {
</Dialog>
) : null;
const aiAnnouncementModal = aiAnnouncementModalOpen ? (
<Dialog
open={aiAnnouncementModalOpen}
onClose={handleCloseAiAnnouncement}
TransitionComponent={Zoom}
TransitionProps={{ timeout: 300 }}
PaperProps={{
style: {
background: theme.palette.DialogStyle.backgroundColor,
minWidth: isMobile ? "90vw" : 780,
maxWidth: isMobile ? "90vw" : 860,
borderRadius: 8,
overflow: "hidden",
transformOrigin: "center",
},
}}
>
<DialogTitle style={{ position: "relative", padding: 0, margin: 0 }}>
<IconButton
style={{
position: "absolute",
top: 10,
right: 10,
}}
onClick={
() => {
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "close_announcement",
label: userdata?.active_org?.id || userdata?.id || "",
});
}
handleCloseAiAnnouncement();
}
}
aria-label="Close"
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent
sx={{
height: "380px",
overflow: "hidden",
}}
>
{/* Main two-column layout */}
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "row" },
gap: { xs: 3, sm: 4 },
alignItems: "stretch",
p: { xs: 2.5, sm: 3 },
mt: 1,
}}
>
{/* Left: steps image (38%) */}
<Box
sx={{
flex: { xs: "0 0 auto", sm: "0 0 38%" },
maxWidth: { xs: "100%", sm: "38%" },
}}
>
<Box
component="img"
src="/aiGenerateWorkflowSteps.svg"
alt="AI workflow generation steps"
sx={{
width: "100%",
height: {xs: "auto", md: "80%"},
marginLeft: -2,
objectFit: "contain",
borderRadius: "6px",
}}
/>
</Box>
{/* Right: content (62%) */}
<Box
sx={{
flex: { xs: "1 1 auto", sm: "0 0 62%" },
maxWidth: { xs: "100%", sm: "62%" },
display: "flex",
flexDirection: "column",
gap: { xs: 1.5, sm: 2 },
py: 1.2,
}}
>
{/* NEW badge */}
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: 1,
py: 0.5,
px: 1.25,
border: "1px solid #2bc07e",
color: "#f85a3e",
background: "transparent",
borderRadius: 999,
width: "fit-content",
fontSize: 10,
fontWeight: 600,
mb: { xs: 0.5, sm: 1 },
}}
>
<AutoAwesomeIcon sx={{ fontSize: 16, color: "#2bc07e" }} />
<Box
component="span"
sx={{
color: "#2bc07e",
}}
>
NEW
</Box>
</Box>
{/* Title */}
<Typography
sx={{
fontWeight: 700,
lineHeight: 1.25,
fontFamily: theme.typography.fontFamily,
fontSize: { xs: "1.25rem", sm: "1.5rem" },
}}
>
Introducing AI Workflow Generation
</Typography>
{/* Body text */}
<Typography
variant="body2"
sx={{
lineHeight: 1.7,
fontFamily: theme.typography.fontFamily,
}}
>
Simply describe what you want your workflow to do, and our AI
will automatically generate the workflow for you.
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: theme.typography.fontFamily }}
>
<strong>Quick start:</strong> Create Workflow Describe AI
Generate Done.
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: theme.typography.fontFamily }}
>
For self-hosted setups, see the{" "}
<Box
component="a"
href="/docs/AI#self-hosting-models"
target="_blank"
rel="noopener noreferrer"
sx={{ color: "#ff8544", textDecoration: "underline" }}
>
setup docs
</Box>
</Typography>
{/* CTA button */}
<Box sx={{ display: "flex", mt: { xs: 2, sm: 2.5 } }}>
<Button
variant="contained"
color="primary"
onClick={dismissAiAnnouncement}
disableElevation
sx={{
py: 1.1,
px: 2.7,
textTransform: "none",
mt: 3,
borderRadius: "8px",
fontSize: 14,
width: { xs: "100%", sm: "auto" },
}}
>
Let's try it out
</Button>
</Box>
</Box>
</Box>
</DialogContent>
</Dialog>
) : null;
const deleteModal = deleteModalOpen ? (
<Dialog
open={deleteModalOpen}
@@ -1170,13 +1558,13 @@ const Workflows2 = (props) => {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
}}
>
<DialogTitle>
<div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}>
@@ -1351,6 +1739,53 @@ const Workflows2 = (props) => {
window.location.reload();
}
}, []);
useEffect(() => {
if (workflows?.length === 0) {
return
}
if (workflowTimelines?.length > 0) {
return
}
//if (isLoggedIn !== true) {
// return
//}
const results = Promise.all(
workflows.slice(0,16).map((workflow, index) => {
return fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => response.json())
})
)
results.then((res) => {
var newarray = []
for (var resKey in res) {
const result = res[resKey]
if (result?.timeline === undefined || result?.timeline === null) {
continue
}
newarray.push({
"id": result.id,
"timeline": result.timeline,
})
}
setWorkflowTimelines(newarray)
})
}, [workflows])
const getAvailableWorkflows = (amount) => {
var storageWorkflows = []
@@ -1407,18 +1842,29 @@ const Workflows2 = (props) => {
toast.info("No workflows found in this org. Feel free to look into our public workflows!" , {
timeout: 7500,
})
setCurrTab(2)
}
localStorage.setItem("workflows", "[]")
setWorkflows([])
setFilteredWorkflows([])
}
var newarray = []
var backupWf = []
var backgroundWf = []
for (var wfkey in responseJson) {
const wf = responseJson[wfkey]
if (wf.public === true || wf.hidden === true) {
if (wf?.public === true || wf?.hidden === true) {
continue
}
if (wf?.background_processing === true) {
backgroundWf.push(wf)
continue
}
if (wf?.backup_config?.onprem_backup === true) {
backupWf.push(wf)
continue
@@ -1427,6 +1873,10 @@ const Workflows2 = (props) => {
newarray.push(wf)
}
if (backgroundWf.length > 0) {
setBackgroundWorkflows(backgroundWf)
}
if (backupWf.length > 0) {
setBackupWorkflows(backupWf)
}
@@ -1659,11 +2109,9 @@ const Workflows2 = (props) => {
const paperAppStyle = {
minHeight: 146,
maxHeight: 146,
overflow: "hidden",
width: "100%",
color: "white",
display: "flex",
fontFamily: theme.typography?.fontFamily,
boxSizing: "border-box",
position: "relative",
@@ -2449,26 +2897,25 @@ const Workflows2 = (props) => {
var orgName = "";
var orgId = "";
var imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
cursor: "pointer",
marginRight: 10,
}
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
image =
@@ -2492,6 +2939,44 @@ const Workflows2 = (props) => {
}
}
var triggerfound = false
var triggerstarted = false
var relevantTrigger = {}
for (var triggerkey in data?.triggers) {
const trigger = data?.triggers[triggerkey]
if (trigger?.trigger_type === "WEBHOOK") {
triggerfound = true
image = wfTriggers[0].large_image
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
}
} else if (trigger?.trigger_type === "SCHEDULE") {
triggerfound = true
image = wfTriggers[1].large_image
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
}
}
}
if (!triggerfound) {
image = ""
}
var selectedCategory = ""
if (data.usecase_ids !== undefined && data.usecase_ids !== null && data.usecase_ids.length > 0 && usecases !== null && usecases !== undefined && usecases.length > 0) {
const oldcolor = boxColor.valueOf()
@@ -2534,6 +3019,7 @@ const Workflows2 = (props) => {
}
image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle} /> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle} />
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "Shuffle"
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
data.objectID = data.id
@@ -2546,10 +3032,12 @@ const Workflows2 = (props) => {
}
}
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return (
<div style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}>
<Paper square style={paperAppStyle}>
{selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
<div
@@ -2558,11 +3046,12 @@ const Workflows2 = (props) => {
position: "absolute",
top: 0,
left: 0,
height: paperAppStyle.minHeight,
width: 3,
backgroundColor: boxColor,
borderRadius: "0 100px 0 0",
fontFamily: theme.typography?.fontFamily,
height: "100%",
}}
onClick={() => {
addFilter(selectedCategory)
@@ -2577,17 +3066,27 @@ const Workflows2 = (props) => {
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
{currTab === 2 ? null :
<Tooltip title={`Org "${orgName}". Click to edit image.`} placement="bottom">
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div
styl={{ cursor: "pointer" }}
style={{ cursor: "" }}
onClick={() => {
navigate("/admin")
//navigate("/admin")
}}
>
{image}
{image?.includes("data:image") ?
<img
alt={orgName}
src={image}
style={imageStyle}
/>
:
image
}
</div>
</Tooltip>
}
<Tooltip arrow
onMouseEnter={() => {
/*
@@ -2695,7 +3194,7 @@ const Workflows2 = (props) => {
style={{
height: 24,
width: 24,
filter: themeMode === "dark" ? "brightness(0.6)" : "brightness(0.9)",
filter: themeMode === "dark" ? "brightness(0.6)" : "brightness(1)",
cursor: "pointer",
}}
onClick={() => {
@@ -2716,7 +3215,7 @@ const Workflows2 = (props) => {
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
filter: themeMode === "dark" ? "brightness(0.6)" : "brightness(1)",
cursor: "pointer",
}}
onClick={() => {
@@ -2874,7 +3373,8 @@ const Workflows2 = (props) => {
})
: null}
</Grid>
{data.actions !== undefined && data.actions !== null && type !== "public" ? (
{type !== "public" ? (
<div style={{ position: "absolute", top: 10, right: 10, }}>
<IconButton
aria-label="more"
@@ -2891,7 +3391,7 @@ const Workflows2 = (props) => {
{(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ?
<Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 50, right: 8, }}>
<div style={{ position: "absolute", top: 80, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2908,8 +3408,8 @@ const Workflows2 = (props) => {
: null}
{(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ?
<Tooltip title={`Explore more than ${data?.validation?.errors?.length} notifications. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top">
<div style={{ position: "absolute", top: 85, right: 8, }}>
<Tooltip title={`Explore more than ${data?.validation?.errors?.length} notifications for this workflow. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top">
<div style={{ position: "absolute", top: 40, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2919,19 +3419,38 @@ const Workflows2 = (props) => {
}}
style={{
padding: "0px",
color: "#979797",
transparency: 0.5,
}}
color="primary"
>
<ErrorOutlineIcon style={{
marginRight: 2,
}} />
<ErrorOutlineIcon
style={{
color: "#f86a3e",
marginRight: 2,
}}
/>
</IconButton>
</div>
</Tooltip>
: null}
</Grid>
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
<div style={{ margin: "40px 10px 0px 10px", paddingTop: 0, borderTop: "1px solid rgba(255,255,255,0.3)", }}>
<LineChartWrapper
inputname={""}
keys={foundTimeline?.timeline}
height={100}
width={100}
border={false}
color={"#808080"}
/>
</div>
}
</Paper>
</div>
)
}
@@ -4414,16 +4933,33 @@ const Workflows2 = (props) => {
{backupWorkflows.length > 0 &&
<Tab
label={`Onprem Backup (${backupWorkflows.length})`}
value={3}
style={{
...tabStyle,
borderLeft: "1px solid rgba(255,255,255,0.3)",
marginLeft: 25,
...(currTab === 3 ? tabActive : {})
}}
/>
}
{backgroundWorkflows.length > 0 &&
<Tab
label={`Background Processes`}
value={4}
style={{
...tabStyle,
borderLeft: "1px solid rgba(255,255,255,0.3)",
borderRight: "1px solid rgba(255,255,255,0.3)",
marginLeft: 25,
...(currTab === 4 ? tabActive : {})
}}
/>
}
<Tab
label="Org Forms"
value={5}
onClick={() => {
navigate("/forms")
}}
@@ -4431,7 +4967,7 @@ const Workflows2 = (props) => {
...tabStyle,
marginRight: 0,
marginLeft: 25,
...(currTab === 4 ? tabActive : {})
...(currTab === 5 ? tabActive : {})
}}
/>
</Tabs>
@@ -4664,7 +5200,23 @@ const Workflows2 = (props) => {
paddingRight: 1,
gap: 4
}}>
<Tooltip title="Explore Workflow Runs" placement="top">
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
onClick={() => {
const newView = !showExecutionStats
localStorage.setItem("showExecutionStats", newView)
setShowExecutionStats(!showExecutionStats)
}}
disabled={currTab === 2}
>
<BarChartIcon />
</IconButton>
</Tooltip>
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => navigate("/workflows/debug")}
@@ -4817,6 +5369,28 @@ const Workflows2 = (props) => {
)
})}
{currTab === 4 && backgroundWorkflows.map((data, index) => {
// Shouldn't be a part of this list
if (data.public === true) {
return null
}
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
})}
{
currTab !== 1 ? null :
myWorkflows.length === 0 ?
@@ -5212,10 +5786,10 @@ const Workflows2 = (props) => {
</ShepherdTour>
*/}
<DropzoneWrapper onDrop={uploadFile} WorkflowView={WorkflowView} />
{/* {modalView} */}
{deleteModal}
{exportVerifyModal}
{publishModal}
{aiAnnouncementModal}
{workflowDownloadModalOpen}
{/*!drawerOpen ?
@@ -5279,8 +5853,10 @@ const Workflows2 = (props) => {
width: '100%',
height: '100%',
}
// Maybe use gridview or something, idk
return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>;
// return <div style={isSafari ? safariStyle : {minHeight: "80vh",}}>{loadedCheck}</div>;
};
@@ -1,2 +0,0 @@
# AWS Lambda forwarder to Shuffle
This function is made to forward S3 notifications to Shuffle to run a workflow when an object is made or updated.
@@ -1,22 +0,0 @@
import json
import urllib.parse
import urllib3
import os
print('Loading function')
def lambda_handler(event, context):
# Get the object from the event and show its content type
bucket = event['Records'][0]['s3']['bucket']['name']
webhook = os.environ.get("SHUFFLE_WEBHOOK")
if not webhook:
return "No webhook environment defined: SHUFFLE_WEBHOOK"
http = urllib3.PoolManager()
ret = http.request('POST', webhook, body=json.dumps(event["Records"][0]).encode("utf-8"))
if ret.status != 200:
return "Bad status code for webhook: %d" % ret.status
print("Status code: %d\nData: %s" % (ret.status, ret.data))
return "Successfully started with data %s" % ret.data
-19
View File
@@ -1,19 +0,0 @@
#docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ELASTICSEARCH_USERNAME=frikky -e ELASTICSEARCH_PASSWORD=likeme -e xpack.security.enabled=true docker.elastic.co/elasticsearch/elasticsearch:7.12.1
#docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.12.1
#
#
#
#echo "\nWaiting for 1.5 minute, then adding data"
#sleep 90
#echo "\nSlept 90 seconds: ADDING DATA"
#curl -XPOST http://localhost:9200/_security/user/frikky -H "Content-Type: application/json" -d '{"enabled": true, "email": "frikky@shuffler.io"}'
#curl -XPOST -u frikky:likeme http://localhost:9200/samples/_doc -H "Content-Type: application/json" -d '{"src": "122.14.137.67", "dst": "103.35.191.16", "message": "alert", "md5": "CAEF973033E593C625FB2AA34F7026DC", "sha256": "DB1AEC5222075800EDA75D7205267569679B424E5C58A28102417F46D3B5790D", "hits": 0}'
#echo
#curl -XPOST -u frikky:likeme http://localhost:9200/samples/_doc -H "Content-Type: application/json" -d '{"src": "134.119.219.71", "dst": "103.35.191.41", "message": "alert", "md5": "9498FF82A64FF445398C8426ED63EA5B", "sha256": "8B2E701E91101955C73865589A4C72999AEABC11043F712E05FDB1C17C4AB19A", "hits": 0}'
#
#echo
#curl -XPOST -u frikky:likeme http://localhost:9200/samples2/_doc -H "Content-Type: application/json" -d '{"src": "122.14.137.67", "dst": "103.35.191.16", "message": "alert", "md5": "CAEF973033E593C625FB2AA34F7026DC", "sha256": "DB1AEC5222075800EDA75D7205267569679B424E5C58A28102417F46D3B5790D"}'
#echo
#curl -XPOST -u frikky:likeme http://localhost:9200/samples2/_doc -H "Content-Type: application/json" -d '{"src": "134.119.219.71", "dst": "103.35.191.41", "message": "alert", "md5": "9498FF82A64FF445398C8426ED63EA5B", "sha256": "8B2E701E91101955C73865589A4C72999AEABC11043F712E05FDB1C17C4AB19A"}'
#echo
@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -1,24 +0,0 @@
apiVersion: v2
name: shuffle
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "2.0.0"
@@ -1,22 +0,0 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "shuffle.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "shuffle.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "shuffle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "shuffle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
@@ -1,62 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "shuffle.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "shuffle.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "shuffle.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "shuffle.labels" -}}
helm.sh/chart: {{ include "shuffle.chart" . }}
{{ include "shuffle.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "shuffle.selectorLabels" -}}
app.kubernetes.io/name: {{ include "shuffle.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "shuffle.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "shuffle.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -1,253 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}frontend
namespace: {{ .Values.namespace | quote }}
spec:
replicas: 1
selector:
matchLabels:
service: shuffle
app: shuffle-frontend
template:
metadata:
labels:
service: shuffle
app: shuffle-frontend
spec:
containers:
- name: shuffle-frontend
image: ghcr.io/frikky/shuffle-frontend:nightly
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
env:
- name: BACKEND_HOSTNAME
value: backend-service
- name: TZ
value: Asia/Shanghai
ports:
- name: http
containerPort: 80
hostPort: 3001
- name: https
containerPort: 443
hostname: shuffle-frontend
restartPolicy: Always
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}backend
namespace: {{ .Values.namespace | quote }}
spec:
replicas: 1
selector:
matchLabels:
service: shuffle
app: shuffle-backend
template:
metadata:
labels:
service: shuffle
app: shuffle-backend
spec:
containers:
- name: shuffle-backend
image: ghcr.io/frikky/shuffle-backend:nightly
env:
- name: BACKEND_HOSTNAME
value: "backend-service"
- name: BACKEND_PORT
value: "5001"
- name: ENVIRONMENT_NAME
value: "Shuffle"
- name: HTTPS_PROXY
- name: HTTP_PROXY
- name: ORG_ID
value: "Shuffle"
- name: OUTER_HOSTNAME
value: "backend-service"
- name: SHUFFLE_APP_FORCE_UPDATE
value: "false"
- name: SHUFFLE_APP_HOTLOAD_FOLDER
value: "/shuffle-apps"
- name: SHUFFLE_APP_HOTLOAD_LOCATION
value: "/shuffle-apps"
- name: SHUFFLE_CONTAINER_AUTO_CLEANUP
value: "false"
- name: SHUFFLE_DEFAULT_APIKEY
- name: SHUFFLE_DEFAULT_PASSWORD
- name: SHUFFLE_DEFAULT_USERNAME
- name: SHUFFLE_DOWNLOAD_AUTH_BRANCH
- name: SHUFFLE_DOWNLOAD_AUTH_PASSWORD
- name: SHUFFLE_DOWNLOAD_AUTH_USERNAME
- name: SHUFFLE_DOWNLOAD_WORKFLOW_BRANCH
- name: SHUFFLE_DOWNLOAD_WORKFLOW_LOCATION
- name: SHUFFLE_DOWNLOAD_WORKFLOW_PASSWORD
- name: SHUFFLE_DOWNLOAD_WORKFLOW_USERNAME
- name: SHUFFLE_ELASTIC
value: "true"
- name: SHUFFLE_OPENSEARCH_APIKEY
- name: SHUFFLE_OPENSEARCH_CERTIFICATE_FILE
- name: SHUFFLE_OPENSEARCH_CLOUDID
- name: SHUFFLE_OPENSEARCH_PASSWORD
- name: SHUFFLE_OPENSEARCH_PROXY
- name: SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY
value: "true"
- name: SHUFFLE_OPENSEARCH_URL
value: http://opensearch-service:9200
- name: SHUFFLE_OPENSEARCH_USERNAME
value: ""
- name: SHUFFLE_PASS_APP_PROXY
value: "FALSE"
- name: SHUFFLE_PASS_WORKER_PROXY
value: "FALSE"
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
- mountPath: /shuffle-apps
name: shuffle-app-hotload-location
- mountPath: /shuffle-files
name: shuffle-file-location
hostname: shuffle-backend
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
- name: shuffle-app-hotload-location
hostPath:
path: /data/kubernetes/shuffle-apps
type: DirectoryOrCreate
- name: shuffle-file-location
hostPath:
path: /data/kubernetes/shuffle-files
type: DirectoryOrCreate
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}orborus
namespace: {{ .Values.namespace | quote }}
spec:
replicas: 1
selector:
matchLabels:
service: shuffle
app: shuffle-orborus
template:
metadata:
labels:
service: shuffle
app: shuffle-orborus
spec:
containers:
- name: shuffle-orborus
image: ghcr.io/frikky/shuffle-orborus:nightly
env:
- name: RUNNING_MODE
value: kubernetes
- name: BASE_URL
value: http://backend-service:5001
- name: CLEANUP
value: "false"
- name: DOCKER_API_VERSION
value: "1.40"
- name: ENVIRONMENT_NAME
value: Shuffle
- name: HTTPS_PROXY
- name: HTTP_PROXY
- name: ORG_ID
value: Shuffle
- name: SHUFFLE_APP_SDK_VERSION
value: 0.8.97
- name: SHUFFLE_BASE_IMAGE_NAME
value: frikky
- name: SHUFFLE_BASE_IMAGE_REGISTRY
value: ghcr.io
- name: SHUFFLE_BASE_IMAGE_TAG_SUFFIX
value: "-0.8.80"
- name: SHUFFLE_ORBORUS_EXECUTION_TIMEOUT
value: "600"
- name: SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY
value: "50"
- name: SHUFFLE_PASS_WORKER_PROXY
value: "TRUE"
- name: SHUFFLE_WORKER_VERSION
value: nightly
- name: TZ
value: Asia/Shanghai
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
hostname: shuffle-orborus
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
#---
#apiVersion: apps/v1
#kind: Deployment
#metadata:
# name: {{ .Values.name }}opensearch
# namespace: {{ .Values.namespace | quote }}
#spec:
# replicas: 1
# selector:
# matchLabels:
# service: shuffle
# app: shuffle-opensearch
# template:
# metadata:
# labels:
# service: shuffle
# app: shuffle-opensearch
# spec:
# nodeSelector:
# node.bdlab-venus.com/opensearch: available
# initContainers:
# - name: permissions-fix
# image: frikky/busybox
# #volumeMounts:
# # - name: opensearch-claim0
# # mountPath: /usr/share/elasticsearch/data
# command: [ 'chown' ]
# args: [ '1000:1000', '/usr/share/elasticsearch/data' ]
# containers:
# - name: shuffle-opensearch
# image: opensearchproject/opensearch:1.0.1
# env:
# - name: TZ
# value: Asia/Shanghai
# - name: bootstrap.memory_lock
# value: "false"
# - name: OPENSEARCH_JAVA_OPTS
# value: "-Xms1024m -Xmx1024m"
# - name: opendistro_security.disabled
# value: "true"
# - name: cluster.routing.allocation.disk.threshold_enabled
# value: "false"
# - name: cluster.name
# value: shuffle-cluster
# - name: node.name
# value: opensearch-service
# - name: discovery.seed_hosts
# value: opensearch-service
# - name: cluster.initial_master_nodes
# value: opensearch-service
# volumeMounts:
# - mountPath: /usr/share/opensearch/data
# name: opensearch-claim0
#volumes:
# - name: opensearch-claim0
# persistentVolumeClaim:
# claimName: opensearch-claim0
# volumeMounts:
# - mountPath: /usr/share/opensearch/data
# readOnly: true
# name: db-location
# volumes:
# - name: db-location
# hostPath:
# path: /data/kubernetes/shuffle-opensearch
# type: DirectoryOrCreate
@@ -1,28 +0,0 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "shuffle.fullname" . }}
labels:
{{- include "shuffle.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "shuffle.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
@@ -1,61 +0,0 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "shuffle.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "shuffle.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,24 +0,0 @@
#apiVersion: v1
#kind: PersistentVolume
#metadata:
# name: opensearch-claim0
# labels:
# app: opensearch-claim0
#spec:
# capacity:
# storage: "10G"
# volumeMode: Filesystem
# persistentVolumeReclaimPolicy: Retain
# storageClassName: local-storage
# accessModes:
# - "ReadWriteOnce"
# local:
# path: "/data/kubernetes/shuffle-opensearch"
# nodeAffinity:
# required:
# nodeSelectorTerms:
# - matchExpressions:
# - key: node.dollar.com/opensearch
# operator: In
# values:
# - available
@@ -1,17 +0,0 @@
#apiVersion: v1
#kind: PersistentVolumeClaim
#metadata:
# name: opensearch-claim0
# namespace: {{ .Values.namespace }}
# labels:
# app: opensearch-claim0
#spec:
# selector:
# matchLabels:
# app: opensearch-claim0
# accessModes:
# - ReadWriteOnce
# storageClassName: local-storage
# resources:
# requests:
# storage: 5Gi
@@ -1,52 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: {{ .Values.namespace | quote }}
spec:
ports:
- name: "5001"
port: 5001
targetPort: 5001
selector:
app: shuffle-backend
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: {{ .Values.namespace | quote }}
spec:
type: NodePort
externalTrafficPolicy: Local
ports:
- name: "3001"
port: 3001
nodePort: 3001
targetPort: 80
- name: "3443"
port: 3443
nodePort: 3443
targetPort: 443
protocol: TCP
selector:
app: shuffle-frontend
#---
#apiVersion: v1
#kind: Service
#metadata:
# name: opensearch-service
# namespace: {{ .Values.namespace | quote }}
#spec:
# type: NodePort
# externalTrafficPolicy: Local
# ports:
# - name: "9200"
# port: 9200
# targetPort: 9200
# nodePort: 9200
# protocol: TCP
# selector:
# app: shuffle-opensearch
@@ -1,12 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "shuffle.serviceAccountName" . }}
labels:
{{- include "shuffle.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "shuffle.fullname" . }}-test-connection"
labels:
{{- include "shuffle.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "shuffle.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
@@ -1,82 +0,0 @@
# Default values for shuffle.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
@@ -1,11 +0,0 @@
curl -XPUT -u admin:admin https://localhost:9200/_cluster/settings -H "Content-Type:application/json" -k -d \
'{
"transient": {
"cluster.routing.allocation.disk.threshold_enabled": false
}
}'
curl -XPUT -u admin:admin https://localhost:9200/_all/_settings -H "Content-Type: application/json" -k -d \
'{
"index.blocks.read_only_allow_delete": null
}'
-1
View File
@@ -1 +0,0 @@
shuffle-database/nodes
@@ -1,131 +0,0 @@
version: '3.4'
services:
backend:
image: ghcr.io/frikky/shuffle-backend:nightly
#hostname: shuffle-backend
environment:
BACKEND_HOSTNAME: backend
OUTER_HOSTNAME: backend
BACKEND_PORT: '5001'
HTTPS_PROXY: ''
HTTP_PROXY: ''
SHUFFLE_APP_DOWNLOAD_LOCATION: https://github.com/frikky/shuffle-apps
SHUFFLE_APP_FORCE_UPDATE: 'false'
SHUFFLE_APP_HOTLOAD_FOLDER: /shuffle-apps
SHUFFLE_APP_HOTLOAD_LOCATION: ./shuffle-apps
DATASTORE_EMULATOR_HOST: "shuffle-database:8000"
DOCKER_API_VERSION: '1.40'
SHUFFLE_BASE_IMAGE_NAME: frikky
SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io
SHUFFLE_BASE_IMAGE_TAG_SUFFIX: '-0.9.30'
SHUFFLE_CONTAINER_AUTO_CLEANUP: 'true'
SHUFFLE_DEFAULT_APIKEY: ''
SHUFFLE_FILE_LOCATION: /shuffle-files
SHUFFLE_OPENSEARCH_APIKEY: ''
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE: ''
SHUFFLE_OPENSEARCH_CLOUDID: ''
SHUFFLE_OPENSEARCH_PROXY: ''
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY: 'true'
SHUFFLE_OPENSEARCH_URL: http://opensearch:9200
SHUFFLE_PASS_APP_PROXY: 'FALSE'
SHUFFLE_PASS_WORKER_PROXY: 'TRUE'
SHUFFLE_ELASTIC: 'true'
#SHUFFLE_ENCRYPTION_MODIFIER:
ports:
- "5001:5001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./shuffle-apps:/shuffle-apps
- ./shuffle-files:/shuffle-files
networks:
- shuffle_prod
#- reverseproxy
depends_on:
- opensearch
logging:
driver: json-file
frontend:
image: ghcr.io/frikky/shuffle-frontend:nightly
healthcheck:
test: curl -fs http://localhost:80 || exit 1
interval: 30s
timeout: 5s
retries: 3
ports:
- "3001:80"
- "3443:443"
networks:
- shuffle_prod
#- reverseproxy
environment:
- "BACKEND_HOSTNAME=backend"
depends_on:
- backend
deploy:
update_config:
order: start-first
opensearch:
image: opensearchproject/opensearch:1.1.0
healthcheck:
test: curl -fs http://localhost:9200/_cat/health || exit 1
interval: 30s
timeout: 5s
retries: 3
environment:
- bootstrap.memory_lock=false
- "OPENSEARCH_JAVA_OPTS=-Xms1024m -Xmx1024m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- plugins.security.disabled=true
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.name=shuffle-cluster
- node.name=opensearch
- discovery.seed_hosts=opensearch
- cluster.initial_master_nodes=opensearch
- node.store.allow_mmap=false
volumes:
- ./shuffle-database:/usr/share/opensearch/data:rw
networks:
- shuffle_prod
#- reverseproxy
logging:
driver: json-file
orborus:
image: ghcr.io/frikky/shuffle-orborus:nightly
#hostname: shuffle-orborus
environment:
#SHUFFLE_WORKER_VERSION: nightly
SHUFFLE_APP_SDK_VERSION: 0.8.97
SHUFFLE_WORKER_VERSION: nightly
BASE_URL: http://backend:5001
#BASE_URL: http://192.168.86.37:5001
CLEANUP: 'true'
DOCKER_API_VERSION: '1.40'
ENVIRONMENT_NAME: Shuffle
HTTPS_PROXY: ''
HTTP_PROXY: ''
ORG_ID: Shuffle
SHUFFLE_BASE_IMAGE_NAME: frikky
SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io
SHUFFLE_BASE_IMAGE_TAG_SUFFIX: -0.8.80
SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY: '50'
SHUFFLE_ORBORUS_EXECUTION_TIMEOUT: '800'
SHUFFLE_PASS_APP_PROXY: 'FALSE'
SHUFFLE_PASS_WORKER_PROXY: 'TRUE'
SHUFFLE_SCALE_REPLICAS: 5
SHUFFLE_SWARM_NETWORK_NAME: shuffle_prod
SHUFFLE_SWARM_CONFIG: "run"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- shuffle_prod
#- reverseproxy
logging:
driver: json-file
networks:
shuffle_prod:
driver: overlay
external: true
#reverseproxy:
# driver: overlay
# #external: true
-1
View File
@@ -1 +0,0 @@
docker network create -d overlay shuffle_prod
-39
View File
@@ -1,39 +0,0 @@
version: '3.4'
services:
orborus:
image: ghcr.io/frikky/shuffle-orborus:nightly
#hostname: shuffle-orborus
environment:
#SHUFFLE_WORKER_VERSION: nightly
SHUFFLE_APP_SDK_VERSION: 0.8.97
SHUFFLE_WORKER_VERSION: nightly
BASE_URL: http://<BACKEND>:5001
#BASE_URL: http://192.168.86.37:5001
CLEANUP: 'true'
DOCKER_API_VERSION: '1.40'
ENVIRONMENT_NAME: Shuffle
HTTPS_PROXY: ''
HTTP_PROXY: ''
ORG_ID: Shuffle
SHUFFLE_BASE_IMAGE_NAME: frikky
SHUFFLE_BASE_IMAGE_REGISTRY: ghcr.io
SHUFFLE_BASE_IMAGE_TAG_SUFFIX: -0.8.80
SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY: '50'
SHUFFLE_ORBORUS_EXECUTION_TIMEOUT: '800'
SHUFFLE_PASS_APP_PROXY: 'FALSE'
SHUFFLE_PASS_WORKER_PROXY: 'TRUE'
SHUFFLE_SCALE_REPLICAS: 5
SHUFFLE_SWARM_NETWORK_NAME: shuffle_prod
SHUFFLE_SWARM_CONFIG: "run"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- shuffle_prod
#- reverseproxy
logging:
driver: json-file
networks:
shuffle_prod:
driver: overlay
external: true
-4
View File
@@ -1,4 +0,0 @@
docker swarm init
chown 1000:1000 -R shuffle-database/
docker network create -d overlay shuffle_prod
docker stack deploy --compose-file=docker-compose.yml shuffle_swarm
@@ -1,4 +0,0 @@
docker swarm init
chown 1000:1000 -R shuffle-database/
docker network create -d overlay shuffle_prod
docker stack deploy --compose-file=orborus.yml shuffle_orborus
-3
View File
@@ -1,3 +0,0 @@
docker stack rm shuffle_swarm
#
-36
View File
@@ -1,36 +0,0 @@
#!/bin/sh
# Created by Shuffle, AS. <frikky@shuffler.io>.
WPYTHON_BIN="framework/python/bin/python3"
SCRIPT_PATH_NAME="$0"
DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)"
SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})"
case ${DIR_NAME} in
*/active-response/bin | */wodles*)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
*/bin)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py"
;;
*/integrations)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
esac
${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@"
@@ -1,206 +0,0 @@
#!/usr/bin/env python3
# Created by Shuffle, AS. <frikky@shuffler.io>.
# Based on the Slack integration using Webhooks
import json
import sys
import time
import os
try:
import requests
from requests.auth import HTTPBasicAuth
except Exception as e:
print("No module 'requests' found. Install: pip install requests")
sys.exit(1)
# ADD THIS TO ossec.conf configuration:
# <integration>
# <name>custom-shuffle</name>
# <hook_url>http://<IP>:3001/api/v1/hooks/<HOOK_ID></hook_url>
# <level>3</level>
# <alert_format>json</alert_format>
# </integration>
# Global vars
debug_enabled = False
pwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
json_alert = {}
now = time.strftime("%a %b %d %H:%M:%S %Z %Y")
# Set paths
log_file = '{0}/logs/integrations.log'.format(pwd)
try:
with open("/tmp/shuffle_start.txt", "w+") as tmp:
tmp.write("Script started")
except:
pass
def main(args):
debug("# Starting")
# Read args
alert_file_location = args[1]
webhook = args[3]
debug("# Webhook")
debug(webhook)
debug("# File location")
debug(alert_file_location)
# Load alert. Parse JSON object.
try:
with open(alert_file_location) as alert_file:
json_alert = json.load(alert_file)
except:
debug("# Alert file %s doesn't exist" % alert_file_location)
debug("# Processing alert")
try:
debug(json_alert)
except Exception as e:
debug("Failed getting json_alert %s" % e)
sys.exit(1)
debug("# Generating message")
msg = generate_msg(json_alert)
if isinstance(msg, str):
if len(msg) == 0:
return
debug(msg)
debug("# Sending message")
try:
with open("/tmp/shuffle_end.txt", "w+") as tmp:
tmp.write("Script done pre-msg sending")
except:
pass
send_msg(msg, webhook)
def debug(msg):
if debug_enabled:
msg = "{0}: {1}\n".format(now, msg)
print(msg)
f = open(log_file, "a")
f.write(msg)
f.close()
# Skips container kills to stop self-recursion
def filter_msg(alert):
# These are things that recursively happen because Shuffle starts Docker containers
skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928", "5710"]
if alert["rule"]["id"] in skip:
return False
#try:
# if "docker" in alert["rule"]["description"].lower() and "
#msg['text'] = alert.get('full_log')
#except:
# pass
#msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A"
return True
def generate_msg(alert):
if not filter_msg(alert):
print("Skipping rule %s" % alert["rule"]["id"])
return ""
level = alert['rule']['level']
if (level <= 4):
severity = 1
elif (level >= 5 and level <= 7):
severity = 2
else:
severity = 3
msg = {}
msg['severity'] = severity
msg['pretext'] = "WAZUH Alert"
msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A"
msg['text'] = alert.get('full_log')
msg['rule_id'] = alert["rule"]["id"]
msg['timestamp'] = alert["timestamp"]
msg['id'] = alert['id']
msg["all_fields"] = alert
#msg['fields'] = []
# msg['fields'].append({
# "title": "Agent",
# "value": "({0}) - {1}".format(
# alert['agent']['id'],
# alert['agent']['name']
# ),
# })
#if 'agentless' in alert:
# msg['fields'].append({
# "title": "Agentless Host",
# "value": alert['agentless']['host'],
# })
#msg['fields'].append({"title": "Location", "value": alert['location']})
#msg['fields'].append({
# "title": "Rule ID",
# "value": "{0} _(Level {1})_".format(alert['rule']['id'], level),
#})
#attach = {'attachments': [msg]}
return json.dumps(msg)
def send_msg(msg, url):
debug("# In send msg")
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
res = requests.post(url, data=msg, headers=headers, verify=False)
debug("# After send msg: %s" % res)
if __name__ == "__main__":
try:
# Read arguments
bad_arguments = False
if len(sys.argv) >= 4:
msg = '{0} {1} {2} {3} {4}'.format(
now,
sys.argv[1],
sys.argv[2],
sys.argv[3],
sys.argv[4] if len(sys.argv) > 4 else '',
)
#debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug')
debug_enabled = True
else:
msg = '{0} Wrong arguments'.format(now)
bad_arguments = True
# Logging the call
try:
f = open(log_file, 'a')
except:
f = open(log_file, 'w+')
f.write("")
f.close()
f = open(log_file, 'a')
f.write(msg + '\n')
f.close()
if bad_arguments:
debug("# Exiting: Bad arguments. Inputted: %s" % sys.argv)
sys.exit(1)
# Main function
main(sys.argv)
except Exception as e:
debug(str(e))
raise
-6
View File
@@ -1,6 +0,0 @@
<integration>
<name>custom-shuffle</name>
<level>9</level>
<hook_url>http://<IP>:<PORT>/api/v1/hooks/webhook_hookid</hook_url>
<alert_format>json</alert_format>
</integration>
+66 -72
View File
@@ -478,81 +478,74 @@ The password should be provided with the `SHUFFLE_OPENSEARCH_PASSWORD` env varia
### worker Parameters
| Name | Description | Value |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `worker.image.registry` | worker image registry | `ghcr.io` |
| `worker.image.repository` | worker image repository | `shuffle/shuffle-worker` |
| `worker.image.tag` | worker image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `worker.image.digest` | worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `worker.podSecurityContext.enabled` | Enable worker pods' Security Context | `true` |
| `worker.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for worker pods | `Always` |
| `worker.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for worker pods | `[]` |
| `worker.podSecurityContext.supplementalGroups` | Set filesystem extra groups for worker pods | `[]` |
| `worker.podSecurityContext.fsGroup` | Set fsGroup in worker pods' Security Context | `1001` |
| `worker.containerSecurityContext.enabled` | Enabled worker container' Security Context | `true` |
| `worker.containerSecurityContext.seLinuxOptions` | Set SELinux options in worker container | `{}` |
| `worker.containerSecurityContext.runAsUser` | Set runAsUser in worker container' Security Context | `1001` |
| `worker.containerSecurityContext.runAsGroup` | Set runAsGroup in worker container' Security Context | `1001` |
| `worker.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in worker container' Security Context | `true` |
| `worker.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in worker container' Security Context | `true` |
| `worker.containerSecurityContext.privileged` | Set privileged in worker container' Security Context | `false` |
| `worker.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in worker container' Security Context | `false` |
| `worker.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in worker container | `["ALL"]` |
| `worker.containerSecurityContext.seccompProfile.type` | Set seccomp profile in worker container | `RuntimeDefault` |
| `worker.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `worker.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `worker.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `worker.serviceAccount.automountServiceAccountToken` | Automount service account token for the worker service account | `true` |
| `worker.serviceAccount.imagePullSecrets` | Add image pull secrets to the worker service account | `[]` |
| `worker.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `worker.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `worker.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `worker.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `worker.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `worker.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
| Name | Description | Value |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `worker.image.registry` | worker image registry | `ghcr.io` |
| `worker.image.repository` | worker image repository | `shuffle/shuffle-worker` |
| `worker.image.tag` | worker image tag (immutable tags are recommended, defaults to appVersion) | `""` |
| `worker.image.digest` | worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` |
| `worker.resourcesPreset` | Set worker container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if worker.resources is set (worker.resources is recommended for production). | `nano` |
| `worker.resources` | Set worker container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `worker.podSecurityContext.enabled` | Enable worker pods' Security Context | `true` |
| `worker.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for worker pods | `Always` |
| `worker.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for worker pods | `[]` |
| `worker.podSecurityContext.supplementalGroups` | Set filesystem extra groups for worker pods | `[]` |
| `worker.podSecurityContext.fsGroup` | Set fsGroup in worker pods' Security Context | `1001` |
| `worker.containerSecurityContext.enabled` | Enabled worker container' Security Context | `true` |
| `worker.containerSecurityContext.seLinuxOptions` | Set SELinux options in worker container | `{}` |
| `worker.containerSecurityContext.runAsUser` | Set runAsUser in worker container' Security Context | `1001` |
| `worker.containerSecurityContext.runAsGroup` | Set runAsGroup in worker container' Security Context | `1001` |
| `worker.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in worker container' Security Context | `true` |
| `worker.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in worker container' Security Context | `true` |
| `worker.containerSecurityContext.privileged` | Set privileged in worker container' Security Context | `false` |
| `worker.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in worker container' Security Context | `false` |
| `worker.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in worker container | `["ALL"]` |
| `worker.containerSecurityContext.seccompProfile.type` | Set seccomp profile in worker container | `RuntimeDefault` |
| `worker.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `worker.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `worker.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `worker.serviceAccount.automountServiceAccountToken` | Automount service account token for the worker service account | `true` |
| `worker.serviceAccount.imagePullSecrets` | Add image pull secrets to the worker service account | `[]` |
| `worker.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `worker.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `worker.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `worker.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `worker.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `worker.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
### app Parameters
| Name | Description | Value |
| ------------------------------------------------- | ---------------------------------------------------------------------------------- | ------ |
| `app.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `app.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `app.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `app.serviceAccount.automountServiceAccountToken` | Automount service account token for the app service account | `true` |
| `app.serviceAccount.imagePullSecrets` | Add image pull secrets to the app service account | `[]` |
| `app.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `app.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `app.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
| `app.exposedContainerPort` | The port that shuffle app containers will listen on for new requests. | `80` |
| `app.podSecurityContext.enabled` | Enable app pods' Security Context | `true` |
| `app.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for app pods | `Always` |
| `app.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for app pods | `[]` |
| `app.podSecurityContext.supplementalGroups` | Set filesystem extra groups for app pods | `[]` |
| `app.podSecurityContext.fsGroup` | Set fsGroup in app pods' Security Context | `1001` |
| `app.containerSecurityContext.enabled` | Enabled app container' Security Context | `true` |
| `app.containerSecurityContext.seLinuxOptions` | Set SELinux options in app container | `{}` |
| `app.containerSecurityContext.runAsUser` | Set runAsUser in app container' Security Context | `1001` |
| `app.containerSecurityContext.runAsGroup` | Set runAsGroup in app container' Security Context | `1001` |
| `app.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in app container' Security Context | `true` |
| `app.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in app container' Security Context | `true` |
| `app.containerSecurityContext.privileged` | Set privileged in app container' Security Context | `false` |
| `app.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in app container' Security Context | `false` |
| `app.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in app container | `["ALL"]` |
| `app.containerSecurityContext.seccompProfile.type` | Set seccomp profile in app container | `RuntimeDefault` |
| `app.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `app.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `app.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `app.serviceAccount.automountServiceAccountToken` | Automount service account token for the app service account | `true` |
| `app.serviceAccount.imagePullSecrets` | Add image pull secrets to the app service account | `[]` |
| `app.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `app.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `app.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
| Name | Description | Value |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `app.resourcesPreset` | Set app container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if app.resources is set (app.resources is recommended for production). | `nano` |
| `app.resources` | Set app container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` |
| `app.podSecurityContext.enabled` | Enable app pods' Security Context | `true` |
| `app.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for app pods | `Always` |
| `app.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for app pods | `[]` |
| `app.podSecurityContext.supplementalGroups` | Set filesystem extra groups for app pods | `[]` |
| `app.podSecurityContext.fsGroup` | Set fsGroup in app pods' Security Context | `1001` |
| `app.containerSecurityContext.enabled` | Enabled app container' Security Context | `true` |
| `app.containerSecurityContext.seLinuxOptions` | Set SELinux options in app container | `{}` |
| `app.containerSecurityContext.runAsUser` | Set runAsUser in app container' Security Context | `1001` |
| `app.containerSecurityContext.runAsGroup` | Set runAsGroup in app container' Security Context | `1001` |
| `app.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in app container' Security Context | `true` |
| `app.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in app container' Security Context | `true` |
| `app.containerSecurityContext.privileged` | Set privileged in app container' Security Context | `false` |
| `app.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in app container' Security Context | `false` |
| `app.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in app container | `["ALL"]` |
| `app.containerSecurityContext.seccompProfile.type` | Set seccomp profile in app container | `RuntimeDefault` |
| `app.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` |
| `app.serviceAccount.name` | The name of the ServiceAccount to use. | `""` |
| `app.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` |
| `app.serviceAccount.automountServiceAccountToken` | Automount service account token for the app service account | `true` |
| `app.serviceAccount.imagePullSecrets` | Add image pull secrets to the app service account | `[]` |
| `app.rbac.create` | Specifies whether RBAC resources should be created | `true` |
| `app.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` |
| `app.networkPolicy.allowExternal` | Don't require server label for connections | `true` |
| `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` |
| `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` |
| `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` |
| `app.exposedContainerPort` | The port that shuffle app containers will listen on for new requests. | `80` |
### Traffic Exposure Parameters
@@ -651,3 +644,4 @@ The password should be provided with the `SHUFFLE_OPENSEARCH_PASSWORD` env varia
### Other Parameters
@@ -13,7 +13,66 @@ data:
TZ: "{{ .Values.shuffle.timezone }}"
BASE_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.backend.containerPorts.http }}"
KUBERNETES_NAMESPACE: "{{ .Release.Namespace }}"
KUBERNETES_SERVICE_ACCOUNT: {{ include "shuffle.orborus.serviceAccount.name" . }}
SHUFFLE_WORKER_IMAGE: "{{ include "shuffle.worker.image" . }}"
REGISTRY_URL: "{{ .Values.shuffle.appRegistry }}"
SHUFFLE_SWARM_CONFIG: run
# Shuffle worker configuration
SHUFFLE_WORKER_IMAGE: {{ include "shuffle.worker.image" . | quote }}
SHUFFLE_WORKER_SERVICE_ACCOUNT_NAME: {{ include "shuffle.worker.serviceAccount.name" . | quote }}
{{- if .Values.worker.podSecurityContext.enabled }}
SHUFFLE_WORKER_POD_SECURITY_CONTEXT: {{ omit .Values.worker.podSecurityContext "enabled" | mustToJson | quote }}
{{- end }}
{{- if .Values.worker.containerSecurityContext.enabled }}
SHUFFLE_WORKER_CONTAINER_SECURITY_CONTEXT: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.worker.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }}
{{- end }}
# Shuffle worker resources
{{- $workerResources := (.Values.worker.resources | default (include "common.resources.preset" (dict "type" .Values.worker.resourcesPreset)) | fromYaml) -}}
{{- if $workerResources.requests.cpu }}
SHUFFLE_WORKER_CPU_REQUEST: {{ $workerResources.requests.cpu | quote }}
{{- end }}
{{- if $workerResources.requests.memory}}
SHUFFLE_WORKER_MEMORY_REQUEST: {{ $workerResources.requests.memory | quote }}
{{- end }}
{{- if (index $workerResources.requests "ephemeral-storage") }}
SHUFFLE_WORKER_EPHEMERAL_STORAGE_REQUEST: {{ (index $workerResources.requests "ephemeral-storage") | quote }}
{{- end }}
{{- if $workerResources.limits.cpu }}
SHUFFLE_WORKER_CPU_LIMIT: {{ $workerResources.limits.cpu | quote }}
{{- end }}
{{- if $workerResources.limits.memory}}
SHUFFLE_WORKER_MEMORY_LIMIT: {{ $workerResources.limits.memory | quote }}
{{- end }}
{{- if (index $workerResources.limits "ephemeral-storage") }}
SHUFFLE_WORKER_EPHEMERAL_STORAGE_LIMIT: {{ (index $workerResources.limits "ephemeral-storage") | quote }}
{{- end }}
# Shuffle app configuration
SHUFFLE_APP_EXPOSED_PORT: {{ .Values.app.exposedContainerPort | quote }}
SHUFFLE_APP_SERVICE_ACCOUNT_NAME: {{ include "shuffle.app.serviceAccount.name" . | quote }}
{{- if .Values.app.podSecurityContext.enabled }}
SHUFFLE_APP_POD_SECURITY_CONTEXT: {{ omit .Values.app.podSecurityContext "enabled" | mustToJson | quote }}
{{- end }}
{{- if .Values.app.containerSecurityContext.enabled }}
SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.app.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }}
{{- end }}
# Shuffle app resources
{{- $appResources := (.Values.app.resources | default (include "common.resources.preset" (dict "type" .Values.app.resourcesPreset)) | fromYaml) -}}
{{- if $appResources.requests.cpu }}
SHUFFLE_APP_CPU_REQUEST: {{ $appResources.requests.cpu | quote }}
{{- end }}
{{- if $appResources.requests.memory}}
SHUFFLE_APP_MEMORY_REQUEST: {{ $appResources.requests.memory | quote }}
{{- end }}
{{- if (index $appResources.requests "ephemeral-storage") }}
SHUFFLE_APP_EPHEMERAL_STORAGE_REQUEST: {{ (index $appResources.requests "ephemeral-storage") | quote }}
{{- end }}
{{- if $appResources.limits.cpu }}
SHUFFLE_APP_CPU_LIMIT: {{ $appResources.limits.cpu | quote }}
{{- end }}
{{- if $appResources.limits.memory}}
SHUFFLE_APP_MEMORY_LIMIT: {{ $appResources.limits.memory | quote }}
{{- end }}
{{- if (index $appResources.limits "ephemeral-storage") }}
SHUFFLE_APP_EPHEMERAL_STORAGE_LIMIT: {{ (index $appResources.limits "ephemeral-storage") | quote }}
{{- end }}
@@ -86,28 +86,8 @@ spec:
value: kubernetes
- name: IS_KUBERNETES
value: "true"
- name: SHUFFLE_WORKER_SERVICE_ACCOUNT_NAME
value: {{ include "shuffle.worker.serviceAccount.name" . }}
- name: SHUFFLE_APP_EXPOSED_PORT
value: {{ .Values.app.exposedContainerPort | quote }}
{{- if .Values.worker.podSecurityContext.enabled }}
- name: SHUFFLE_WORKER_POD_SECURITY_CONTEXT
value: {{ omit .Values.worker.podSecurityContext "enabled" | mustToJson | quote }}
{{- end }}
{{- if .Values.worker.containerSecurityContext.enabled }}
- name: SHUFFLE_WORKER_CONTAINER_SECURITY_CONTEXT
value: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.worker.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }}
{{- end }}
- name: SHUFFLE_APP_SERVICE_ACCOUNT_NAME
value: {{ include "shuffle.app.serviceAccount.name" . }}
{{- if .Values.app.podSecurityContext.enabled }}
- name: SHUFFLE_APP_POD_SECURITY_CONTEXT
value: {{ omit .Values.app.podSecurityContext "enabled" | mustToJson | quote }}
{{- end }}
{{- if .Values.app.containerSecurityContext.enabled }}
- name: SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT
value: {{ include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.app.containerSecurityContext "context" $) | fromYaml | mustToJson | quote }}
{{- end }}
- name: SHUFFLE_SWARM_CONFIG
value: run
{{- if .Values.orborus.extraEnvVars }}
{{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVars "context" $) | nindent 12 }}
{{- end }}
@@ -2084,6 +2084,16 @@
}
}
},
"resourcesPreset": {
"type": "string",
"description": "Set worker container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if worker.resources is set (worker.resources is recommended for production).",
"default": "nano"
},
"resources": {
"type": "object",
"description": "Set worker container requests and limits for different resources like CPU or memory (essential for production workloads)",
"default": {}
},
"podSecurityContext": {
"type": "object",
"properties": {
@@ -2259,6 +2269,16 @@
"app": {
"type": "object",
"properties": {
"resourcesPreset": {
"type": "string",
"description": "Set app container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if app.resources is set (app.resources is recommended for production).",
"default": "nano"
},
"resources": {
"type": "object",
"description": "Set app container requests and limits for different resources like CPU or memory (essential for production workloads)",
"default": {}
},
"podSecurityContext": {
"type": "object",
"properties": {
@@ -1340,6 +1340,24 @@ worker:
tag: ""
digest: ""
## worker resource requests and limits
## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
## @param worker.resourcesPreset Set worker container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if worker.resources is set (worker.resources is recommended for production).
## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15
##
resourcesPreset: "nano"
## @param worker.resources Set worker container requests and limits for different resources like CPU or memory (essential for production workloads)
## Example:
## resources:
## requests:
## cpu: 2
## memory: 512Mi
## limits:
## cpu: 3
## memory: 1024Mi
##
resources: {}
## Configure Pods Security Context
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
## @param worker.podSecurityContext.enabled Enable worker pods' Security Context
@@ -1444,6 +1462,24 @@ worker:
## @section app Parameters
##
app:
## app resource requests and limits
## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
## @param app.resourcesPreset Set app container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if app.resources is set (app.resources is recommended for production).
## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15
##
resourcesPreset: "nano"
## @param app.resources Set app container requests and limits for different resources like CPU or memory (essential for production workloads)
## Example:
## resources:
## requests:
## cpu: 2
## memory: 512Mi
## limits:
## cpu: 3
## memory: 1024Mi
##
resources: {}
## Configure Pods Security Context
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
## @param app.podSecurityContext.enabled Enable app pods' Security Context
+12 -10
View File
@@ -7,10 +7,10 @@ toolchain go1.24.4
//replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared
require (
github.com/docker/docker v28.2.2+incompatible
github.com/docker/docker v28.3.3+incompatible
github.com/docker/go-connections v0.5.0
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.84
github.com/shuffle/shuffle-shared v0.9.14
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
)
@@ -58,7 +58,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/frikky/kin-openapi v0.42.0 // indirect
github.com/frikky/schemaless v0.0.16 // indirect
github.com/frikky/schemaless v0.0.20 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
@@ -95,11 +95,13 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opensearch-project/opensearch-go v1.1.0 // indirect
github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect
github.com/osteele/liquid v1.7.0 // indirect
github.com/osteele/tuesday v1.0.3 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/sashabaranov/go-openai v1.40.1 // indirect
github.com/sashabaranov/go-openai v1.40.5 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
@@ -121,13 +123,13 @@ require (
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.236.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
+26 -22
View File
@@ -116,8 +116,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -142,8 +142,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0=
github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4=
github.com/frikky/schemaless v0.0.16 h1:4d2ZktB9xGsAusbbKliOI8TuriSrdIMzD/6ToY3wkz8=
github.com/frikky/schemaless v0.0.16/go.mod h1:jT48kTcmr1q3o8i+8qe7g+eCsbwaz2Q9CjOJevQQzQs=
github.com/frikky/schemaless v0.0.20 h1:S/A2pQcRN9qa2RnufvxwCeM06trjG0JLTF3urt1tFQI=
github.com/frikky/schemaless v0.0.20/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -299,6 +299,10 @@ github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
github.com/osteele/liquid v1.7.0 h1:VsbPSchE5D5S5scylAIvERET4dnCxsO6IDri2oSJ5Dk=
github.com/osteele/liquid v1.7.0/go.mod h1:xU0Z2dn2hOQIEFEWNmeltOmCtfhtoW/2fCyiNQeNG+U=
github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU=
github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
@@ -314,8 +318,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU=
github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.40.5 h1:SwIlNdWflzR1Rxd1gv3pUg6pwPc6cQ2uMoHs8ai+/NY=
github.com/sashabaranov/go-openai v1.40.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
@@ -324,8 +328,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM=
github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk=
github.com/shuffle/shuffle-shared v0.9.14 h1:POkTHO+bByuv8HiKuCMSGgtpDKk86ISr6ooLG8vQfuE=
github.com/shuffle/shuffle-shared v0.9.14/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
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=
@@ -402,8 +406,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
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=
@@ -452,8 +456,8 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
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,8 +473,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -495,14 +499,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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=
@@ -513,8 +517,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -546,8 +550,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+54 -30
View File
@@ -85,12 +85,11 @@ var appContainerSecurityContext = os.Getenv("SHUFFLE_APP_CONTAINER_SECURITY_CONT
// var baseimagename = "docker.pkg.github.com/shuffle/shuffle"
// var baseimagename = "ghcr.io/frikky"
// var baseimagename = "shuffle/shuffle"
var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
var baseimageregistry = os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")
var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
//var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
// Used for cloud with auth
// Used for cloud with auth. Onprem in certain cases too.
var auth = os.Getenv("AUTH")
var org = os.Getenv("ORG")
@@ -497,6 +496,7 @@ func deployServiceWorkers(image string) {
//}
}
// Running 2 by default instead of 1. Higher scale mechanisms - es
replicas := uint64(1)
scaleReplicas := os.Getenv("SHUFFLE_SCALE_REPLICAS")
if len(scaleReplicas) > 0 {
@@ -522,7 +522,7 @@ func deployServiceWorkers(image string) {
}
appReplicas := os.Getenv("SHUFFLE_APP_REPLICAS")
appReplicaCnt := 1
appReplicaCnt := 2
if len(appReplicas) > 0 {
newCnt, err := strconv.Atoi(appReplicas)
if err != nil {
@@ -734,7 +734,7 @@ func deployServiceWorkers(image string) {
var updatedNetworks []swarm.NetworkAttachmentConfig
for _, net := range serviceSpec.Networks {
if net.Target != "shuffle_shuffle" {
updatedNetworks = append(updatedNetworks, net)
updatedNetworks = append(updatedNetworks, net)
}
}
serviceSpec.Networks = updatedNetworks
@@ -813,20 +813,20 @@ func handleBackendImageDownload(ctx context.Context, images string) error {
newImages = append(newImages, curimage)
// Force remove the current image to avoid cached layers
// if swarmConfig == "run" || swarmConfig == "swarm" {
// _, err := dockercli.ImageRemove(ctx, curimage, image.RemoveOptions{
// Force: true,
// PruneChildren: true,
// })
// if swarmConfig == "run" || swarmConfig == "swarm" {
// _, err := dockercli.ImageRemove(ctx, curimage, image.RemoveOptions{
// Force: true,
// PruneChildren: true,
// })
// if err != nil {
// log.Printf("[ERROR] Failed removing image for re-download: %s", err)
// } else {
// log.Printf("[DEBUG] Removed image: %s", curimage)
// }
// } else {
// //log.Printf("[DEBUG] Skipping image removal for %s as swarmConfig is not set to run or swarm. Value: %#v", curimage, swarmConfig)
// }
// if err != nil {
// log.Printf("[ERROR] Failed removing image for re-download: %s", err)
// } else {
// log.Printf("[DEBUG] Removed image: %s", curimage)
// }
// } else {
// //log.Printf("[DEBUG] Skipping image removal for %s as swarmConfig is not set to run or swarm. Value: %#v", curimage, swarmConfig)
// }
err := shuffle.DownloadDockerImageBackend(&http.Client{Timeout: imagedownloadTimeout}, curimage)
if err != nil {
@@ -887,7 +887,7 @@ func handleBackendImageDownload(ctx context.Context, images string) error {
log.Printf("[ERROR] Failed updating service %s with the new image %s: %s. Resp: %#v", service.Spec.Annotations.Name, image, err, resp)
} else {
log.Printf("[DEBUG] Updated service %s with the new image %s. Resp: %#v", service.Spec.Annotations.Name, image, resp)
found = true
if !strings.Contains(fmt.Sprintf("%s", resp), "error") {
@@ -1041,6 +1041,10 @@ func deployK8sWorker(image string, identifier string, env []string) error {
env = append(env, fmt.Sprintf("KUBERNETES_SERVICE_PORT=%s", os.Getenv("KUBERNETES_SERVICE_PORT")))
}
if len(os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")) > 0 {
env = append(env, fmt.Sprintf("SHUFFLE_BASE_IMAGE_REGISTRY=%s", os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")))
}
if len(os.Getenv("REGISTRY_URL")) > 0 {
env = append(env, fmt.Sprintf("REGISTRY_URL=%s", os.Getenv("REGISTRY_URL")))
}
@@ -1065,6 +1069,10 @@ func deployK8sWorker(image string, identifier string, env []string) error {
env = append(env, fmt.Sprintf("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT=%s", appContainerSecurityContext))
}
if len(os.Getenv("SHUFFLE_LOGS_DISABLED")) > 0 {
env = append(env, fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", os.Getenv("SHUFFLE_LOGS_DISABLED")))
}
clientset, _, err := shuffle.GetKubernetesClient()
if err != nil {
log.Printf("[ERROR] Error getting kubernetes client:", err)
@@ -1096,8 +1104,13 @@ func deployK8sWorker(image string, identifier string, env []string) error {
}
}
// Required format:
// url/org/repo/appname:tag
// url/org/repo/appname:tag
//env = append(env, fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", swarmConfig))
env = append(env, fmt.Sprintf("BASE_URL=%s", baseUrl))
env = append(env, fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", swarmConfig))
env = append(env, fmt.Sprintf("SHUFFLE_SWARM_CONFIG=run"))
env = append(env, fmt.Sprintf("WORKER_HOSTNAME=%s", "shuffle-workers"))
if len(kubernetesNamespace) == 0 {
@@ -2044,6 +2057,11 @@ func main() {
log.Printf("[DEBUG] Verbose mode. NOT cleaning up. Cleanup env: %s", cleanupEnv)
}
// Default to 120 instead of default 30
if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) == 0 {
os.Setenv("SHUFFLE_APP_SDK_TIMEOUT", "120")
}
workerTimeout := 600
if workerTimeoutEnv != "" {
tmpInt, err := strconv.Atoi(workerTimeoutEnv)
@@ -2133,16 +2151,17 @@ func main() {
if isKubernetes != "true" {
deployServiceWorkers(workerImage)
err := setBackendToSwarmNetwork(ctx)
if err != nil {
log.Printf("[WARNING] Failed setting backend to swarm network: %s", err)
}
} else {
deployK8sWorker(workerImage, "shuffle-workers", []string{})
runString = "Run: \"kubectl get pods\" for more info"
}
err := setBackendToSwarmNetwork(ctx)
if err != nil {
log.Printf("[WARNING] Failed setting backend to swarm network: %s", err)
}
log.Printf("[DEBUG] Waiting 45 seconds to ensure workers are deployed. %s", runString)
time.Sleep(time.Duration(45) * time.Second)
@@ -2214,7 +2233,6 @@ func main() {
log.Printf("[INFO] Waiting for executions at %s with Environment %#v", fullUrl, environment)
hasStarted := false
for {
if req.Method == "POST" {
@@ -2298,7 +2316,7 @@ func main() {
}
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" && os.Getenv("SHUFFLE_SCALE_REPLICAS") == "" {
// go AutoScale(ctx)
//go AutoScale(ctx)
}
hasStarted = true
}
@@ -3857,7 +3875,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
var options container.StopOptions
for _, containername := range stopContainers {
log.Printf("[INFO] Stopping and removing container %s", containerNames[containername])
dockercli.ContainerStop(ctx, containername, options)
go dockercli.ContainerStop(ctx, containername, options)
removeContainers = append(removeContainers, containername)
}
@@ -3938,7 +3956,13 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
}
}
client := &http.Client{}
client := &http.Client{
//Transport: &http.Transport{
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
//},
Timeout: time.Duration(120 * time.Second),
}
req, err := http.NewRequest(
"POST",
streamUrl,
@@ -4013,7 +4037,7 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
debugCommand := fmt.Sprintf("docker service logs shuffle-workers 2>&1 -f | grep %s", workflowExecution.ExecutionId)
if isKubernetes == "true" {
debugCommand = fmt.Sprintf("kubectl logs -n %s container=shuffle-worker | grep %s", kubernetesNamespace, workflowExecution.ExecutionId)
debugCommand = fmt.Sprintf("kubectl logs -n %s deployment/shuffle-workers | grep %s", kubernetesNamespace, workflowExecution.ExecutionId)
}
log.Printf("[DEBUG] Ran worker from request with execution ID: %s. Worker URL: %s. DEBUGGING:\n%s", workflowExecution.ExecutionId, streamUrl, debugCommand)
+17
View File
@@ -0,0 +1,17 @@
# Worker
A worker implementation in Golang. This runs ALL Shuffle workflows onprem. In general receives jobs from Orborus.
## Development
The ideal way to test the Worker is with a single workflow execution, standalone. Here are some environment variables you can use:
```
# Control what to run
export ENVIRONMENT_NAME=""
export SHUFFLE_CLOUDRUN_URL="https://shuffler.io"
export AUTHORIZATION=""
export EXECUTIONID=""
# Control debugging and shutdown mechanisms
export DEBUG="true"
export SHUFFLE_WORKER_SHUTDOWN_DISABLED="true"
```

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