Executable → Regular
+1
-1
@@ -27,7 +27,7 @@ FROM alpine:latest as certs
|
||||
RUN apk add --update ca-certificates
|
||||
|
||||
# Sets up the final image
|
||||
FROM alpine:3.22.1
|
||||
FROM alpine:3.22.2
|
||||
|
||||
# FIXME: Install cgo because CGO_ENABLED=1 during build
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -4,12 +4,12 @@ go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
//replace github.com/shuffle/opensearch-go => ../../../opensearch-go
|
||||
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
|
||||
|
||||
//replace github.com/frikky/schemaless => ../../../schemaless
|
||||
|
||||
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
||||
//replace github.com/shuffle/opensearch-go => ../../../opensearch-go
|
||||
|
||||
require (
|
||||
cloud.google.com/go/datastore v1.20.0
|
||||
@@ -26,8 +26,8 @@ require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.9.34
|
||||
github.com/shuffle/singul v0.0.17
|
||||
github.com/shuffle/shuffle-shared v0.9.53
|
||||
github.com/shuffle/singul v0.0.20
|
||||
golang.org/x/crypto v0.40.0
|
||||
google.golang.org/api v0.236.0
|
||||
google.golang.org/grpc v1.72.2
|
||||
@@ -75,7 +75,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.22 // indirect
|
||||
github.com/frikky/schemaless v0.0.24 // 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
|
||||
@@ -115,8 +115,6 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.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
|
||||
@@ -128,6 +126,7 @@ require (
|
||||
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/shuffle/opensearch-go/v4 v4.0.0 // 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
|
||||
|
||||
+20
-34
@@ -72,20 +72,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
|
||||
github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
|
||||
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
|
||||
@@ -152,8 +138,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.22 h1:aMc7cc/lr1zpogjGWbY0j6J2f6QqyfPbbW6Y9JgTAqE=
|
||||
github.com/frikky/schemaless v0.0.22/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY=
|
||||
github.com/frikky/schemaless v0.0.24 h1:1bNg96gm82yLbRlDS0haQH/96X3GlCAEuTCSasXIJZE=
|
||||
github.com/frikky/schemaless v0.0.24/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=
|
||||
@@ -223,7 +209,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -265,8 +250,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -325,10 +308,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M=
|
||||
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
|
||||
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
|
||||
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
|
||||
github.com/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=
|
||||
@@ -363,10 +342,12 @@ 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.9.34 h1:4RtGRMoYtBQia2CLbuH7aPFM+EMFXbH0ccIQsBXs8n8=
|
||||
github.com/shuffle/shuffle-shared v0.9.34/go.mod h1:WbLPLyBrh86ieRBgDyjC43M2qBF8a7E8jO8Pv7t8SOk=
|
||||
github.com/shuffle/singul v0.0.17 h1:mxaPtj6z85Nf6tl7L2gwDliTfEZtRQqApuu9iKcP75o=
|
||||
github.com/shuffle/singul v0.0.17/go.mod h1:8c42n1NahhCIPxzLxwp9eYbWkvY4+ct0jfbhkRsRRsY=
|
||||
github.com/shuffle/opensearch-go/v4 v4.0.0 h1:Mh85CD1MwOgXiFFYlzS1llnvdqL3CztRdR1ZT/SLIjU=
|
||||
github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0=
|
||||
github.com/shuffle/shuffle-shared v0.9.53 h1:ykziiRA82FZX0YiuErSmFLQMxvAFi6yUajgRjd3J5xE=
|
||||
github.com/shuffle/shuffle-shared v0.9.53/go.mod h1:g03P/ZM8pUwBbdxW0l9A1siMMuemEQshgDpO9hF23eE=
|
||||
github.com/shuffle/singul v0.0.20 h1:Lz+K4l2GJQ5W6o8ePr/2qlIcGuZuMC7W8Wj/JFw1XDI=
|
||||
github.com/shuffle/singul v0.0.20/go.mod h1:qY3ZmwaNwmRApLWMTqZaNEMXO6X8bjr/zY44xDU62hQ=
|
||||
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=
|
||||
@@ -393,8 +374,18 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
|
||||
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -491,9 +482,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
@@ -536,13 +525,11 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
@@ -554,7 +541,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
|
||||
+51
-25
@@ -14,18 +14,18 @@ import (
|
||||
"crypto/md5"
|
||||
"strconv"
|
||||
|
||||
"os"
|
||||
"io"
|
||||
"log"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"net/http"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -67,6 +67,7 @@ var runningEnvironment = "onprem"
|
||||
|
||||
var syncUrl = "https://shuffler.io"
|
||||
var debug = false
|
||||
|
||||
//var syncUrl = "http://localhost:5002"
|
||||
|
||||
type retStruct struct {
|
||||
@@ -3416,7 +3417,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
|
||||
// Read and copy the baseline Dockerfile
|
||||
dockerfileContent, err := ioutil.ReadFile(dockerfileSource)
|
||||
if err != nil {
|
||||
foundDockerfile := shuffle.GetBaseDockerfile()
|
||||
foundDockerfile := shuffle.GetBaseDockerfile()
|
||||
if len(foundDockerfile) > 0 {
|
||||
dockerfileContent = foundDockerfile
|
||||
} else {
|
||||
@@ -4033,7 +4034,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
|
||||
}
|
||||
|
||||
// Just to prevent it from spamming large outbound requests
|
||||
if shouldBackupData {
|
||||
if shouldBackupData {
|
||||
if org.SyncConfig.WorkflowBackup {
|
||||
workflows, err := shuffle.GetAllWorkflowsByQuery(ctx, foundUser, 250, "")
|
||||
if err != nil {
|
||||
@@ -4067,7 +4068,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
|
||||
}
|
||||
|
||||
// Send stats once every 10 times or so..?
|
||||
// For now, just send every time
|
||||
// For now, just send every time
|
||||
info, err := shuffle.GetOrgStatistics(ctx, org.Id)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed getting org statistics backup for org %s: %s", org.Id, err)
|
||||
@@ -4243,10 +4244,8 @@ func runInitEs(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(os.Getenv("SHUFFLE_OPENSEARCH_URL"), "https") {
|
||||
log.Printf("[INFO] Waiting 30 seconds during init to make sure the opensearch instance is up and running with security features enabled")
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
log.Printf("[INFO] Waiting 30 seconds during init to make sure the opensearch instance is up and running with security features enabled")
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
shuffle.InitOpensearchIndexes()
|
||||
|
||||
@@ -4403,7 +4402,7 @@ func runInitEs(ctx context.Context) {
|
||||
}
|
||||
|
||||
//interval := int(org.SyncConfig.Interval)
|
||||
interval := 30
|
||||
interval := 30
|
||||
if interval == 0 {
|
||||
log.Printf("[WARNING] Skipping org %s because sync isn't set (0).", org.Id)
|
||||
continue
|
||||
@@ -4505,7 +4504,6 @@ func runInitEs(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
respBody, err := ioutil.ReadAll(newresp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed setting respbody %s for execution stop. Status: %d", err, newresp.StatusCode)
|
||||
@@ -4576,7 +4574,7 @@ 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/python-apps")
|
||||
|
||||
|
||||
url = "https://github.com/shuffle/python-apps"
|
||||
//url = ""
|
||||
//return
|
||||
@@ -4662,9 +4660,9 @@ func runInitEs(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Self-cleaning
|
||||
go func() {
|
||||
go func() {
|
||||
cursor := ""
|
||||
cnt := 0
|
||||
cnt := 0
|
||||
newCtx := context.Background()
|
||||
for _, org := range activeOrgs {
|
||||
if len(org.Id) == 0 {
|
||||
@@ -4674,7 +4672,7 @@ func runInitEs(ctx context.Context) {
|
||||
|
||||
log.Printf("[INFO] Starting self-cleanup of cache keys for org %s", org.Id)
|
||||
|
||||
for {
|
||||
for {
|
||||
keys, newCursor, err := shuffle.GetAllCacheKeys(newCtx, org.Id, "", 1000, cursor)
|
||||
if err != nil {
|
||||
//log.Printf("[ERROR] Failed getting all cache keys for cleanup: %s", err)
|
||||
@@ -4958,6 +4956,17 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
resp.Write(b)
|
||||
} else {
|
||||
|
||||
// Delete cache for the sync features
|
||||
cacheKey := fmt.Sprintf("org_sync_features_%s", org.Id)
|
||||
shuffle.DeleteCache(ctx, cacheKey)
|
||||
|
||||
subscriptionCacheKey := fmt.Sprintf("org_subscriptions_%s", org.Id)
|
||||
shuffle.DeleteCache(ctx, subscriptionCacheKey)
|
||||
|
||||
licenseCacheKey := fmt.Sprintf("org_licensed_%s", org.Id)
|
||||
shuffle.DeleteCache(ctx, licenseCacheKey)
|
||||
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully disabled cloud sync for org."}`)))
|
||||
}
|
||||
@@ -5049,12 +5058,29 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
||||
shuffle.SetCache(ctx, cacheKey, featuresBytes, 1800)
|
||||
}
|
||||
|
||||
subscriptionCacheKey := fmt.Sprintf("org_subscriptions_%s", org.Id)
|
||||
subscriptionsBytes, err := json.Marshal(responseData.Subscriptions)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to marshal Subscriptions for cache: %s", err)
|
||||
} else {
|
||||
shuffle.SetCache(ctx, subscriptionCacheKey, subscriptionsBytes, 1800)
|
||||
}
|
||||
|
||||
licenseCacheKey := fmt.Sprintf("org_licensed_%s", org.Id)
|
||||
licensedBytes, err := json.Marshal(responseData.Licensed)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to marshal Licensed for cache: %s", err)
|
||||
} else {
|
||||
|
||||
shuffle.SetCache(ctx, licenseCacheKey, licensedBytes, 1800)
|
||||
}
|
||||
|
||||
org.SyncConfig = shuffle.SyncConfig{
|
||||
Apikey: responseData.SessionKey,
|
||||
Interval: responseData.IntervalSeconds,
|
||||
|
||||
WorkflowBackup: true,
|
||||
AppBackup: true,
|
||||
AppBackup: true,
|
||||
}
|
||||
|
||||
interval := int(responseData.IntervalSeconds)
|
||||
@@ -5677,7 +5703,7 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/dashboards/{key}/widgets", shuffle.HandleNewWidget).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/dashboards/{key}/widgets/{widget_id}", shuffle.HandleGetWidget).Methods("GET", "OPTIONS")
|
||||
|
||||
if (strings.ToLower(os.Getenv("SHUFFLE_DEBUG_MEMORY")) == "true" || strings.ToLower(os.Getenv("DEBUG_MEMORY")) == "true") {
|
||||
if strings.ToLower(os.Getenv("SHUFFLE_DEBUG_MEMORY")) == "true" || strings.ToLower(os.Getenv("DEBUG_MEMORY")) == "true" {
|
||||
log.Printf("[DEBUG] Memory debugging is enabled on /debug/pprof")
|
||||
r.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
r.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
|
||||
|
||||
+57
-48
@@ -20,21 +20,21 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/h2non/filetype"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
newscheduler "github.com/carlescere/scheduler"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/frikky/kin-openapi/openapi3"
|
||||
gyaml "github.com/ghodss/yaml"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
http2 "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
gyaml "github.com/ghodss/yaml"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
)
|
||||
|
||||
var localBase = "http://localhost:5001"
|
||||
@@ -257,7 +257,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// This is really the environment's name - NOT OrgId
|
||||
// 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")
|
||||
@@ -276,7 +276,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// This section is cloud custom for now
|
||||
auth := request.Header.Get("Authorization")
|
||||
if len(auth) == 0 {
|
||||
@@ -305,7 +305,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
// Only works onprem - shared queues across tenants
|
||||
// without tenancy
|
||||
// without tenancy
|
||||
if !found {
|
||||
env, err = shuffle.GetEnvironment(ctx, environment, "")
|
||||
if err != nil {
|
||||
@@ -336,7 +336,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
if len(executionRequests.Data) == 0 {
|
||||
executionRequests.Data = []shuffle.ExecutionRequest{}
|
||||
} else {
|
||||
// Try again? I don't think this is necessary, and shouldn't really ever occur.
|
||||
// Try again? I don't think this is necessary, and shouldn't really ever occur.
|
||||
if len(executionRequests.Data) > 50 {
|
||||
executionRequests.Data = executionRequests.Data[0:49]
|
||||
}
|
||||
@@ -457,16 +457,16 @@ func handleGetWorkflowExecutionResult(resp http.ResponseWriter, request *http.Re
|
||||
|
||||
if workflowExecution.Workflow.Sharing == "form" {
|
||||
newWorkflow := shuffle.Workflow{
|
||||
Name: workflowExecution.Workflow.Name,
|
||||
ID: workflowExecution.Workflow.ID,
|
||||
Owner: workflowExecution.Workflow.Owner,
|
||||
OrgId: workflowExecution.Workflow.OrgId,
|
||||
Name: workflowExecution.Workflow.Name,
|
||||
ID: workflowExecution.Workflow.ID,
|
||||
Owner: workflowExecution.Workflow.Owner,
|
||||
OrgId: workflowExecution.Workflow.OrgId,
|
||||
|
||||
Sharing: workflowExecution.Workflow.Sharing,
|
||||
Sharing: workflowExecution.Workflow.Sharing,
|
||||
Description: workflowExecution.Workflow.Description,
|
||||
InputQuestions: workflowExecution.Workflow.InputQuestions,
|
||||
|
||||
FormControl: workflowExecution.Workflow.FormControl,
|
||||
FormControl: workflowExecution.Workflow.FormControl,
|
||||
}
|
||||
|
||||
workflowExecution.Results = []shuffle.ActionResult{}
|
||||
@@ -765,7 +765,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
if user.Id != workflow.Owner || len(user.Id) == 0 {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
log.Printf("[INFO] User %s is deleting workflow %s as admin. Owner: %s", user.Username, workflow.ID, workflow.Owner)
|
||||
} else {
|
||||
log.Printf("[WARNING] Wrong user (%s) for workflow %s (delete workflow)", user.Username, workflow.ID)
|
||||
@@ -782,7 +782,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
childWorkflows, err := shuffle.ListChildWorkflows(ctx, workflow.ID)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to list child workflows: %s", err)
|
||||
} else {
|
||||
} else {
|
||||
//log.Printf("\n\n[DEBUG] Found %d child workflows for workflow %s\n\n", len(childWorkflows), workflow.ID)
|
||||
|
||||
// Find cookies and append them to request.Header to replicate current request as closely as possible
|
||||
@@ -898,10 +898,10 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
|
||||
}
|
||||
|
||||
/*
|
||||
if !workflow.IsValid {
|
||||
log.Printf("[ERROR] Stopped execution as workflow %s is not valid.", workflow.ID)
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow")
|
||||
}
|
||||
if !workflow.IsValid {
|
||||
log.Printf("[ERROR] Stopped execution as workflow %s is not valid.", workflow.ID)
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow")
|
||||
}
|
||||
*/
|
||||
|
||||
maxExecutionDepth := 10
|
||||
@@ -914,7 +914,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
|
||||
|
||||
workflowExecution, execInfo, _, workflowExecErr := shuffle.PrepareWorkflowExecution(ctx, workflow, request, int64(maxExecutionDepth))
|
||||
if workflowExecErr != nil {
|
||||
if len(workflowExecution.Workflow.Actions) > 0 && len(workflowExecution.Results) > 0 && len(workflowExecution.ExecutionId) > 0 {
|
||||
if len(workflowExecution.Workflow.Actions) > 0 && len(workflowExecution.Results) > 0 && len(workflowExecution.ExecutionId) > 0 {
|
||||
err := shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
|
||||
@@ -928,7 +928,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
|
||||
return shuffle.WorkflowExecution{}, "", nil
|
||||
} else {
|
||||
log.Printf("[ERROR] Failed in prepareExecution: '%s'", workflowExecErr)
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", workflowExecErr), workflowExecErr
|
||||
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", workflowExecErr), workflowExecErr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,7 +1337,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
if user.Id != workflow.Owner || len(user.Id) == 0 {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
log.Printf("[AUDIT] User %s is accessing workflow %s as admin (stop schedule)", user.Username, workflow.ID)
|
||||
} else {
|
||||
log.Printf("[WARNING] Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID)
|
||||
@@ -1505,30 +1505,39 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
func deleteSchedule(ctx context.Context, id string) error {
|
||||
log.Printf("[DEBUG] Should stop schedule %s!", id)
|
||||
func deleteKeySchedule(ctx context.Context, id string) error {
|
||||
err := shuffle.DeleteKey(ctx, "schedules", id)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to delete schedule: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteSchedule(ctx context.Context, id string) error {
|
||||
log.Printf("[DEBUG] Should stop schedule %s!", id)
|
||||
if value, exists := scheduledJobs[id]; exists {
|
||||
// Stops the schedule properly
|
||||
value.Lock()
|
||||
} else {
|
||||
if value, exists := scheduledJobs[id]; exists {
|
||||
// Stops the schedule properly
|
||||
value.Lock()
|
||||
} else {
|
||||
// FIXME - allow it to kind of stop anyway?
|
||||
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.")
|
||||
// FIXME - allow it to kind of stop anyway?
|
||||
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 {
|
||||
// Just stop and delete anyway if not in memory
|
||||
deleteKeySchedule(ctx, id)
|
||||
return errors.New("Can't find the schedule.")
|
||||
}
|
||||
}
|
||||
|
||||
err := deleteKeySchedule(ctx, id)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to stop schedule in db %s: %s", id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1582,7 +1591,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
if user.Id != workflow.Owner || len(user.Id) == 0 {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
if workflow.OrgId == user.ActiveOrg.Id {
|
||||
log.Printf("[INFO] User %s is deleting workflow %s as admin. Owner: %s", user.Username, workflow.ID, workflow.Owner)
|
||||
} else {
|
||||
log.Printf("[WARNING] Wrong user (%s) for workflow %s (schedule start). Owner: %s", user.Username, workflow.ID, workflow.Owner)
|
||||
@@ -2022,7 +2031,7 @@ func loadGithubWorkflows(url, username, password, userId, branch, orgId string)
|
||||
cloneOptions.ReferenceName = plumbing.ReferenceName(branch)
|
||||
}
|
||||
|
||||
cloneOptions = checkGitProxy(cloneOptions)
|
||||
cloneOptions = checkGitProxy(cloneOptions)
|
||||
|
||||
storer := memory.NewStorage()
|
||||
r, err := git.Clone(storer, fs, cloneOptions)
|
||||
@@ -2877,7 +2886,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
|
||||
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" {
|
||||
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)))
|
||||
@@ -2905,7 +2914,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
workflowExecution.ProjectId = ""
|
||||
workflowExecution.ProjectId = ""
|
||||
workflowExecution.Locations = []string{""}
|
||||
|
||||
foundEnv := ""
|
||||
@@ -2923,7 +2932,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
go shuffle.IncrementCache(ctx, workflowExecution.OrgId, "workflow_executions")
|
||||
executionRequest := shuffle.ExecutionRequest{
|
||||
Priority: 15,
|
||||
Priority: 15,
|
||||
ExecutionId: workflowExecution.ExecutionId,
|
||||
WorkflowId: workflowExecution.Workflow.ID,
|
||||
Authorization: workflowExecution.Authorization,
|
||||
@@ -2998,7 +3007,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
|
||||
continue
|
||||
}
|
||||
|
||||
//duringStartup
|
||||
//duringStartup
|
||||
if duringStartup {
|
||||
// Look for names: shuffle tools, http, email, shuffle ai
|
||||
if shuffle.ArrayContains(startupNames, strings.ToLower(file.Name())) {
|
||||
@@ -3366,7 +3375,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
|
||||
|
||||
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)
|
||||
@@ -3402,7 +3411,7 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os.
|
||||
for _, item := range buildLaterList {
|
||||
if len(item.Tags) > 0 && shuffle.ArrayContains(handledImages, item.Tags[0]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
handledImages = append(handledImages, item.Tags[0])
|
||||
|
||||
@@ -3493,7 +3502,7 @@ func LoadSpecificApps(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
cloneOptions = checkGitProxy(cloneOptions)
|
||||
cloneOptions = checkGitProxy(cloneOptions)
|
||||
|
||||
storer := memory.NewStorage()
|
||||
r, err := git.Clone(storer, fs, cloneOptions)
|
||||
|
||||
+4
-5
@@ -122,11 +122,10 @@ services:
|
||||
# - MEMCACHED_MAX_CONNECTIONS=2500
|
||||
# ports:
|
||||
# - 11211:11211
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# memory: 2048M
|
||||
|
||||
# networks:
|
||||
# - shuffle
|
||||
# - swarm_executions
|
||||
|
||||
#docker-socket-proxy:
|
||||
# image: tecnativa/docker-socket-proxy
|
||||
# container_name: docker-socket-proxy
|
||||
|
||||
Executable → Regular
+1
-1
@@ -27,7 +27,7 @@ COPY ./*.json /usr/src/app/
|
||||
RUN npm run build --loglevel verbose 2>&1
|
||||
|
||||
# Production environment
|
||||
FROM nginx:1.29.0
|
||||
FROM nginx:1.29.3
|
||||
|
||||
RUN mkdir -p /usr/share/nginx/html/build
|
||||
RUN mkdir -p /usr/share/nginx/html/css
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import theme, { getTheme } from '../theme.jsx';
|
||||
import { Button, Skeleton, Tooltip } from '@mui/material';
|
||||
import { Box, Button, Skeleton, Tooltip, Typography } from '@mui/material';
|
||||
import { Index } from 'react-instantsearch-dom';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -265,7 +265,7 @@ const AdminNavBar = (props) => {
|
||||
: selectedOrganization?.image;
|
||||
|
||||
return (
|
||||
!isOrgLoaded && !isUserDataLoaded ? <Loader /> :
|
||||
!isOrgLoaded || !isUserDataLoaded ? <Loader /> :
|
||||
<Wrapper>
|
||||
<div style={{ flexDirection: 'column', width: 220, }}>
|
||||
<nav style={{ padding: '25px 25px 3px 25px', height: isCloud ? "calc(100% - 60px)" : "calc(100% - 30px)" , fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: theme.palette.platformColor, color: '#9CA3AF' }}>
|
||||
@@ -358,7 +358,6 @@ const AdminNavBar = (props) => {
|
||||
export default AdminNavBar;
|
||||
|
||||
const Loader = () => {
|
||||
const dummyItems = Array.from({ length: 6 });
|
||||
const dummyNavItems = Array.from({ length: 6 });
|
||||
const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding'];
|
||||
const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context);
|
||||
@@ -366,9 +365,7 @@ const Loader = () => {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '100vh',
|
||||
width: '100%',
|
||||
maxWidth: '1200px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80,
|
||||
@@ -446,15 +443,49 @@ const Loader = () => {
|
||||
</div>
|
||||
<div style={{ flex: 1, padding: '24px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', margin: '0 auto', alignItems: 'flex-start' }}>
|
||||
<Skeleton variant='square' width="200px" height="200px" sx={{ marginBottom: '20px' }} />
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%', marginTop: '50px' }}>
|
||||
{dummyItems.map((_, index) => (
|
||||
<div key={index} style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Skeleton width={'400px'} height="36px" />
|
||||
</div>
|
||||
))}
|
||||
<Typography variant="h2">
|
||||
<Skeleton variant="text" width={289} height={29}/>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{mb: 2}}>
|
||||
<Skeleton variant="text" width={550} height={22}/>
|
||||
</Typography>
|
||||
<div style={{display: 'flex'}}>
|
||||
<Skeleton variant='square' width="145px" height="145px" sx={{ borderRadius: '8px' }} />
|
||||
<div style={{flexDirection: 'column', marginLeft: '10px', justifyContent: 'center', marginTop: 'auto', marginBottom: 'auto'}}>
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
sx={{ marginBottom: '10px', height: '35px', width: '109px', borderRadius: '4px' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Box sx={{ minWidth: 700, marginTop: 5}}>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||
<Skeleton variant='text' width="40%" height={30} />
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2, mb: 3 }}>
|
||||
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}} />
|
||||
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}} />
|
||||
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}}/>
|
||||
</Box>
|
||||
|
||||
|
||||
<Skeleton variant="rectangular" height={89} sx={{ mb: 4, borderRadius: "4px"}} />
|
||||
|
||||
|
||||
<Skeleton width="30%" height={28} sx={{ mb: 2 }} />
|
||||
|
||||
<Skeleton width="90%" height={20} sx={{ mb: 1 }} />
|
||||
<Skeleton width="70%" height={20} sx={{ mb: 3 }} />
|
||||
|
||||
|
||||
<Skeleton variant="rectangular" height={35} sx={{ mb: 4, borderRadius: '4px' }} />
|
||||
|
||||
<Skeleton width="40%" height={35} sx={{ mb: 1, borderRadius: '4px' }} />
|
||||
<Skeleton width="80%" height={35} sx={{ mb: 1, borderRadius: '4px' }} />
|
||||
<Skeleton variant="rectangular" height={56} sx={{ mb: 3, borderRadius: '4px' }} />
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import throttle from "lodash/throttle";
|
||||
import {getTheme} from "../theme.jsx";
|
||||
import { validateJson, collapseField, } from "../views/Workflows.jsx";
|
||||
import { validateJson, collapseField, } from "../views/Workflows2.jsx";
|
||||
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
ClearRefinements,
|
||||
connectStateResults
|
||||
} from "react-instantsearch-dom";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
import aa from "search-insights";
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import aa from 'search-insights'
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
@@ -38,6 +38,8 @@ const Appsearch = props => {
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integration any app"
|
||||
|
||||
const parsedInputHeight = inputHeight === undefined || inputHeight === null ? 295 : inputHeight
|
||||
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
@@ -91,7 +93,7 @@ const Appsearch = props => {
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: parsedInputHeight, minHeight: parsedInputHeight, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{hits.map((data, index) => {
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? theme.palette.hoverColor : theme.palette.textFieldStyle.backgroundColor,
|
||||
@@ -182,7 +184,7 @@ const Appsearch = props => {
|
||||
const CustomHits = connectHits(InputHits)
|
||||
|
||||
return (
|
||||
<div style={{width: isMobile ? null : 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
|
||||
<div style={{width: isMobile ? null : 287, minHeight: parsedInputHeight, maxHeight: parsedInputHeight, padding: "16px 16px 64px 16px", alignItems: "center", }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
|
||||
@@ -3141,7 +3141,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
|
||||
orgId: subOrg.id,
|
||||
limit: subOrg?.sync_features?.app_executions?.limit || "N/A",
|
||||
usage: stat?.monthly_app_executions || "N/A",
|
||||
workflows_usage: stat?.total_workflow_executions || "N/A",
|
||||
workflows_usage: stat?.monthly_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,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+548
-105
@@ -6,6 +6,7 @@ import theme from '../theme.jsx';
|
||||
import Markdown from 'react-markdown'
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AppSearch from "../components/AppSearch1.jsx";
|
||||
|
||||
@@ -37,14 +38,24 @@ const ChatBot = (props) => {
|
||||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||||
const [inputAuth, setInputAuth] = useState([])
|
||||
const [forceReauthentication, setForceReauthentication] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState("atomic");
|
||||
const [selectedType, setSelectedType] = useState("support");
|
||||
|
||||
const [appname, setAppname] = useState("");
|
||||
const [threadId, setThreadId] = useState("");
|
||||
const [runId, setRunId] = useState("");
|
||||
|
||||
// New state for thread management
|
||||
const [isLoadingThread, setIsLoadingThread] = useState(false);
|
||||
const [threadError, setThreadError] = useState("");
|
||||
const [isActiveOrg, setIsActiveOrg] = useState(true);
|
||||
const [threadOrgId, setThreadOrgId] = useState("");
|
||||
const [chatDisabled, setChatDisabled] = useState(false);
|
||||
|
||||
const [showAppSearch, setShowAppSearch] = useState(false);
|
||||
|
||||
// Get thread ID from URL params
|
||||
const { threadId: urlThreadId } = useParams();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const waitingMsg = "Processing..."
|
||||
const viewWidth = isMobile ? "92%" : 800
|
||||
@@ -82,8 +93,159 @@ const ChatBot = (props) => {
|
||||
}
|
||||
}, [appname])
|
||||
|
||||
// Load existing thread if threadId is in URL
|
||||
useEffect(() => {
|
||||
if (urlThreadId && urlThreadId !== threadId) {
|
||||
loadExistingThread(urlThreadId);
|
||||
}
|
||||
}, [urlThreadId]);
|
||||
|
||||
const loadExistingThread = (threadIdToLoad) => {
|
||||
setIsLoadingThread(true);
|
||||
setThreadError("");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/conversation/thread`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ thread_id: threadIdToLoad }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 403) {
|
||||
setThreadError("You are not authorized to view this conversation.");
|
||||
setIsLoadingThread(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
setThreadError("Conversation not found.");
|
||||
setIsLoadingThread(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.success) {
|
||||
setThreadError(data.message || "Failed to load conversation.");
|
||||
setIsLoadingThread(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set thread data - this is crucial for continuing the conversation
|
||||
setThreadId(data.thread_id);
|
||||
console.log("Loaded existing thread:", data.thread_id);
|
||||
|
||||
// Transform API message format to UI format
|
||||
const transformedMessages = (data.messages || []).map((msg, index) => ({
|
||||
id: `${data.thread_id}_${index}`,
|
||||
status: msg.role === "user" ? "sent" : "received",
|
||||
message: msg.content,
|
||||
timestamp: msg.timestamp
|
||||
}));
|
||||
|
||||
setMessages(transformedMessages);
|
||||
setThreadOrgId(data.thread_org_id);
|
||||
setIsActiveOrg(data.is_active_org);
|
||||
|
||||
if (!data.is_active_org) {
|
||||
setChatDisabled(true);
|
||||
} else {
|
||||
setChatDisabled(false);
|
||||
}
|
||||
|
||||
setIsLoadingThread(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading thread:", error);
|
||||
setThreadError("Failed to load conversation. Please try again.");
|
||||
setIsLoadingThread(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickChangeOrg = (orgId) => {
|
||||
toast.info("Changing active organization - please wait!");
|
||||
|
||||
const data = {
|
||||
org_id: orgId,
|
||||
};
|
||||
|
||||
// Clear org-specific cached data
|
||||
localStorage.setItem("globalUrl", "");
|
||||
localStorage.setItem("getting_started_sidebar", "open");
|
||||
localStorage.removeItem("workflows");
|
||||
localStorage.removeItem("apps");
|
||||
localStorage.removeItem("dashboard_onboarding_complete");
|
||||
localStorage.removeItem("dashboard_onboarding_completed");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status !== 200) {
|
||||
console.log("Error in response");
|
||||
} else {
|
||||
// Clear additional cached data
|
||||
localStorage.removeItem("apps");
|
||||
localStorage.removeItem("workflows");
|
||||
localStorage.removeItem("userinfo");
|
||||
localStorage.removeItem("lastTabOpenByUser");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(function (responseJson) {
|
||||
if (responseJson.success === true) {
|
||||
if (
|
||||
responseJson?.region_url !== undefined &&
|
||||
responseJson?.region_url !== null &&
|
||||
responseJson?.region_url.length > 0
|
||||
) {
|
||||
localStorage.setItem("globalUrl", responseJson.region_url);
|
||||
}
|
||||
|
||||
if (responseJson["reason"] === "SSO_REDIRECT") {
|
||||
setTimeout(() => {
|
||||
toast.info("Redirecting to SSO login page as SSO is required for this organization.");
|
||||
window.location.href = responseJson["url"];
|
||||
return;
|
||||
}, 2000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
toast.success("Successfully changed active organization - refreshing!");
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
|
||||
toast(responseJson.reason);
|
||||
} else {
|
||||
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error changing: ", error);
|
||||
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
|
||||
});
|
||||
};
|
||||
|
||||
window.title = "Shuffle - New Chat"
|
||||
let navigate = useNavigate();
|
||||
|
||||
// Automatic submit handler based on a lot of stuff :)
|
||||
const handleSubmit = (e, inputmsg) => {
|
||||
@@ -102,6 +264,8 @@ const ChatBot = (props) => {
|
||||
"run_id": runId,
|
||||
}
|
||||
|
||||
console.log("Sending message with thread_id:", threadId);
|
||||
|
||||
if (appname !== undefined && appname !== null && appname !== "") {
|
||||
parsedData["app_name"] = appname
|
||||
}
|
||||
@@ -190,6 +354,12 @@ const ChatBot = (props) => {
|
||||
|
||||
if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") {
|
||||
setThreadId(data.thread_id)
|
||||
|
||||
// Update URL if this is a new thread (not already in URL)
|
||||
if (!urlThreadId && data.thread_id !== threadId) {
|
||||
console.log("Navigating to new thread URL:", data.thread_id);
|
||||
navigate(`/chat/${data.thread_id}`, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (data.success === undefined) {
|
||||
@@ -423,8 +593,6 @@ const ChatBot = (props) => {
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
console.log("Click? ")
|
||||
e.preventDefault();
|
||||
@@ -520,32 +688,38 @@ const ChatBot = (props) => {
|
||||
}
|
||||
|
||||
function Img(props) {
|
||||
return <img style={{ borderRadius: theme.palette?.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
|
||||
return <img style={{
|
||||
borderRadius: 12,
|
||||
maxWidth: "100%",
|
||||
height: "auto",
|
||||
marginTop: 12,
|
||||
marginBottom: 12,
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)"
|
||||
}} alt={props.alt} src={props.src} />;
|
||||
}
|
||||
|
||||
function CodeHandler(props) {
|
||||
//console.log("Codehandler PROPS: ", props)
|
||||
|
||||
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "100%",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
overflowY: "auto",
|
||||
|
||||
// Check if props.inline === true, then do it inline
|
||||
padding: props.inline ? 0 : 15,
|
||||
display: props.inline ? "inline" : "block",
|
||||
backgroundColor: props.inline ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.3)",
|
||||
borderRadius: props.inline ? "4px" : "8px",
|
||||
padding: props.inline ? "2px 6px" : "12px 16px",
|
||||
display: props.inline ? "inline" : "block",
|
||||
margin: props.inline ? "0 2px" : "8px 0",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
overflowX: "auto"
|
||||
}}
|
||||
>
|
||||
<code
|
||||
style={{
|
||||
// Wrap if larger than X
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "auto",
|
||||
whiteSpace: props.inline ? "nowrap" : "pre-wrap",
|
||||
fontSize: props.inline ? "0.9em" : "0.85em",
|
||||
fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
|
||||
color: "rgba(255, 255, 255, 0.9)"
|
||||
}}
|
||||
>{propvalue}</code>
|
||||
</div>
|
||||
@@ -654,7 +828,12 @@ const ChatBot = (props) => {
|
||||
|
||||
const Paragraph = (props) => {
|
||||
return (
|
||||
<p style={{marginTop: 15, marginBottom: 15, }}>
|
||||
<p style={{
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
lineHeight: 1.6,
|
||||
color: "inherit"
|
||||
}}>
|
||||
{props.children}
|
||||
</p>
|
||||
)
|
||||
@@ -677,14 +856,183 @@ const ChatBot = (props) => {
|
||||
|
||||
const chatWindow =
|
||||
<div style={{minWidth: viewWidth, maxWidth: viewWidth, margin: "auto", textAlign: "left", minHeight: 1500, }}>
|
||||
{messages.length === 0 ?
|
||||
<span>
|
||||
<h1>Shuffle AI</h1>
|
||||
{/* Loading state for thread */}
|
||||
{isLoadingThread ? (
|
||||
<div style={{
|
||||
textAlign: "center",
|
||||
marginTop: "30vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<CircularProgress
|
||||
size={48}
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
color: "#ff8544"
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" style={{
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontWeight: 500
|
||||
}}>
|
||||
Loading conversation...
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
marginTop: 8
|
||||
}}>
|
||||
Please wait while we fetch your chat history
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Error state */}
|
||||
{threadError ? (
|
||||
<div style={{
|
||||
textAlign: "center",
|
||||
marginTop: "30vh",
|
||||
padding: "0 20px"
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
borderRadius: 16,
|
||||
padding: 32,
|
||||
border: "1px solid #ff4444",
|
||||
maxWidth: 500,
|
||||
margin: "0 auto",
|
||||
boxShadow: "0 4px 20px rgba(255, 68, 68, 0.1)"
|
||||
}}>
|
||||
<Typography variant="h6" style={{
|
||||
color: "#ff4444",
|
||||
marginBottom: 16,
|
||||
fontWeight: 600,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<span style={{marginRight: 8, fontSize: "1.5em"}}>⚠️</span>
|
||||
Unable to Load Conversation
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{threadError}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isActiveOrg && !threadError ? (
|
||||
<div style={{
|
||||
backgroundColor: "rgba(255, 133, 68, 0.1)",
|
||||
padding: "12px 16px",
|
||||
margin: "16px 0",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255, 133, 68, 0.3)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: isMobile ? "wrap" : "nowrap",
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{display: "flex", alignItems: "center", gap: 10, flex: 1}}>
|
||||
<span style={{fontSize: "1.1em"}}>⚠️</span>
|
||||
<div>
|
||||
<Typography variant="body2" style={{color: "#ff8544", fontWeight: 600, marginBottom: 2}}>
|
||||
Different Organization
|
||||
</Typography>
|
||||
<Typography variant="caption" style={{color: "rgba(255, 255, 255, 0.7)", fontSize: "0.8rem"}}>
|
||||
View only - switch to participate
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: "#ff8544",
|
||||
color: "white",
|
||||
borderRadius: 6,
|
||||
textTransform: "none",
|
||||
fontWeight: 600,
|
||||
padding: "6px 16px",
|
||||
fontSize: "0.85rem",
|
||||
whiteSpace: "nowrap",
|
||||
boxShadow: "none"
|
||||
}}
|
||||
onClick={() => {
|
||||
handleClickChangeOrg(threadOrgId);
|
||||
}}
|
||||
>
|
||||
Switch Org
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showSamples}
|
||||
</span>
|
||||
{!isLoadingThread && !threadError && messages.length === 0 ?
|
||||
<div style={{textAlign: "center", marginTop: "25vh"}}>
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, #ff8544 0%, #ff6b35 100%)`,
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Typography variant="h2" style={{
|
||||
fontWeight: 700,
|
||||
fontSize: isMobile ? "2rem" : "2.5rem",
|
||||
letterSpacing: "-0.02em"
|
||||
}}>
|
||||
Shuffle Support (beta)
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="h6" style={{
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontWeight: 400,
|
||||
marginBottom: 32
|
||||
}}>
|
||||
How can we help you today?
|
||||
</Typography>
|
||||
|
||||
{/* Sample prompts */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
gap: 16,
|
||||
justifyContent: "center",
|
||||
maxWidth: 800,
|
||||
margin: "0 auto",
|
||||
padding: "0 20px"
|
||||
}}>
|
||||
{[
|
||||
"How many incidents did we get last week?",
|
||||
"Answer the last email from Jim about the new project",
|
||||
"Is the IP 1.2.3.4 blocked? If not, block it."
|
||||
].map((sample, index) => (
|
||||
<Card key={index} style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
borderRadius: 12,
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
flex: 1,
|
||||
minWidth: isMobile ? "100%" : "200px"
|
||||
}} onClick={() => setMessage(sample)}>
|
||||
<CardContent style={{padding: 20}}>
|
||||
<Typography variant="body2" style={{
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
lineHeight: 1.5,
|
||||
fontSize: "0.9rem"
|
||||
}}>
|
||||
{sample}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div
|
||||
id="messages-window"
|
||||
@@ -698,140 +1046,235 @@ const ChatBot = (props) => {
|
||||
}}
|
||||
>
|
||||
{messages.map((message, index) => {
|
||||
const float = message.status === "sent" ? "left" : "right";
|
||||
const border = message.status === "error" ? "red" : "rgba(255,255,255,0.3)"
|
||||
|
||||
const hasAction = message.action !== undefined && message.action !== null && message.action !== ""
|
||||
const isUser = message.status === "sent";
|
||||
const isError = message.status === "error";
|
||||
const hasAction = message.action !== undefined && message.action !== null && message.action !== "";
|
||||
|
||||
return (
|
||||
// Make a chat bubble component
|
||||
<div key={index} style={{position: "relative", width: "100%", marginTop: 15, marginLeft: isMobile ? 10 : 0, }}>
|
||||
<Typography variant="body1" style={{display: "flex", backgroundColor: theme.palette.surfaceColor, color: "white", padding: "0px 10px 0px 10px", borderRadius: theme.palette?.borderRadius, float: float, border: `1px solid ${border}`, "cursor": hasAction ? "pointer" : "default", maxWidth: viewWidth-30, overflowWrap: "break-word", whiteSpace: "pre-line" }} onClick={() => {
|
||||
if (!hasAction) {
|
||||
return
|
||||
}
|
||||
<div key={index} style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
marginBottom: 20,
|
||||
display: "flex",
|
||||
justifyContent: isUser ? "flex-end" : "flex-start",
|
||||
marginLeft: isMobile ? 10 : 0,
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: "75%",
|
||||
minWidth: "200px",
|
||||
backgroundColor: isUser ? "#ff8544" : theme.palette.surfaceColor,
|
||||
color: isUser ? "#000000" : "rgba(255, 255, 255, 0.9)",
|
||||
padding: "16px 20px",
|
||||
borderRadius: isUser ? "20px 20px 4px 20px" : "20px 20px 20px 4px",
|
||||
border: isError ? "1px solid #ff4444" : "1px solid rgba(255,255,255,0.1)",
|
||||
cursor: hasAction ? "pointer" : "default",
|
||||
overflowWrap: "break-word",
|
||||
boxShadow: isUser
|
||||
? "0 2px 12px rgba(255, 133, 68, 0.3)"
|
||||
: "0 2px 12px rgba(0, 0, 0, 0.2)",
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": hasAction ? {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: "0 4px 16px rgba(255, 133, 68, 0.4)"
|
||||
} : {}
|
||||
}} onClick={() => {
|
||||
if (!hasAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.action === "login") {
|
||||
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT")
|
||||
} else if (message.action === "app_authentication") {
|
||||
console.log("App auth action!")
|
||||
//setAuthenticationModalOpen(true)
|
||||
} else {
|
||||
console.log("\n\nUnknown click action: ", message.action)
|
||||
}
|
||||
if (message.action === "login") {
|
||||
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT");
|
||||
} else if (message.action === "app_authentication") {
|
||||
console.log("App auth action!");
|
||||
} else {
|
||||
console.log("\n\nUnknown click action: ", message.action);
|
||||
}
|
||||
}}>
|
||||
{message.message === waitingMsg ? <CircularProgress style={{height: 20, width: 20, marginTop: 20, marginRight: 10, }} /> : null}
|
||||
<span>
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
style={{
|
||||
minHeight: 20,
|
||||
marginTop: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Markdown>
|
||||
{message.message === waitingMsg ? (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<CircularProgress size={20} style={{marginRight: 12, color: "rgba(255, 255, 255, 0.7)"}} />
|
||||
<span style={{color: "rgba(255, 255, 255, 0.7)"}}>Processing...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
style={{
|
||||
color: "inherit",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Markdown>
|
||||
|
||||
{message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" ?
|
||||
<Typography variant="body2" style={{color: "rgba(255,255,255,0.5)", marginTop: 5, }}>
|
||||
Thread: {message.thread_id}
|
||||
</Typography>
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
</Typography>
|
||||
{message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" && !isUser ? (
|
||||
<Typography variant="caption" style={{
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
marginTop: 8,
|
||||
display: "block",
|
||||
fontSize: "0.75rem"
|
||||
}}>
|
||||
Thread: {message.thread_id}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message.status === "error" && message.error_message ?
|
||||
<Typography variant="body2" style={{color: "red", }}>
|
||||
{message.status === "error" && message.error_message ? (
|
||||
<Typography variant="body2" style={{
|
||||
color: "#ff4444",
|
||||
marginTop: 8,
|
||||
fontSize: "0.85rem",
|
||||
fontStyle: "italic"
|
||||
}}>
|
||||
{message.error_message}
|
||||
</Typography>
|
||||
: null}
|
||||
) : null}
|
||||
|
||||
{(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ?
|
||||
<div style={{position: "absolute", right: 0, bottom: -100, }}>
|
||||
{(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ? (
|
||||
<div style={{position: "absolute", right: 0, bottom: -100}}>
|
||||
<AppSearch
|
||||
placeholder={"Find your "+message.category+" app"}
|
||||
setNewSelectedApp={setAppname}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showAuthentication}
|
||||
|
||||
<div style={{position: "fixed", bottom: 0, left: 0, width: "100%", zIndex: 100, backgroundColor: theme.palette.platformColor, }}>
|
||||
<div style={{width: viewWidth, margin: "auto", }}>
|
||||
<div style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
zIndex: 100,
|
||||
background: `linear-gradient(180deg, transparent 0%, ${theme.palette.platformColor} 20%)`,
|
||||
backdropFilter: "blur(10px)",
|
||||
borderTop: "1px solid rgba(255, 255, 255, 0.1)"
|
||||
}}>
|
||||
<div style={{width: viewWidth, margin: "auto", padding: "20px 0"}}>
|
||||
{/* Commented out Query Type section for now
|
||||
{messages.length === 0 ?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<div style={{marginBottom: 20}}>
|
||||
<Typography variant="body2" style={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
marginBottom: 12,
|
||||
fontWeight: 500
|
||||
}}>
|
||||
Query Type
|
||||
</Typography>
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
color="secondary"
|
||||
style={{display: "flex", marginTop: 10, }}
|
||||
style={{display: "flex", marginTop: 10}}
|
||||
>
|
||||
{/*
|
||||
<Button
|
||||
fullWidth
|
||||
disabled
|
||||
variant={selectedType === "default" ? "contained" : "outlined"}
|
||||
onClick={() => setSelectedType("default")}
|
||||
>
|
||||
Auto
|
||||
</Button>
|
||||
*/}
|
||||
<Button
|
||||
fullWidth
|
||||
variant={selectedType === "atomic" ? "contained" : "outlined"}
|
||||
onClick={() => setSelectedType("atomic")}
|
||||
>
|
||||
Auto-run action
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={selectedType === "support" ? "contained" : "outlined"}
|
||||
onClick={() => setSelectedType("support")}
|
||||
style={{
|
||||
backgroundColor: selectedType === "support" ? "#ff8544" : "transparent",
|
||||
borderColor: "#ff8544",
|
||||
color: selectedType === "support" ? "white" : "#ff8544",
|
||||
textTransform: "none",
|
||||
fontWeight: 600,
|
||||
borderRadius: "8px",
|
||||
padding: "12px 24px"
|
||||
}}
|
||||
>
|
||||
Support
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
</div>
|
||||
: null}
|
||||
*/}
|
||||
|
||||
<form onSubmit={(e) => handleSubmit(e, message)} style={{bottom: 20, marginTop: 10, marginBottom: isMobile ? 0 : 10, maxWidth: viewWidth, minWidth: viewWidth, }}>
|
||||
<form onSubmit={(e) => handleSubmit(e, message)} style={{
|
||||
marginBottom: isMobile ? 10 : 20,
|
||||
maxWidth: viewWidth,
|
||||
minWidth: viewWidth
|
||||
}}>
|
||||
<TextField
|
||||
id="message"
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
label="Send a message"
|
||||
disabled={loading || chatDisabled}
|
||||
placeholder={chatDisabled ? "Chat disabled - switch organization to participate" : "Type your message..."}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e, message);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
autoFocus={!chatDisabled}
|
||||
multiline
|
||||
maxRows={4}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||
'&:hover': {
|
||||
border: '1px solid rgba(255, 133, 68, 0.4)',
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
border: '1px solid #ff8544',
|
||||
boxShadow: '0 0 0 2px rgba(255, 133, 68, 0.1)'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-input': {
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
padding: '12px 16px',
|
||||
fontSize: '0.95rem'
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
aria-label="send message"
|
||||
disabled={chatDisabled || loading || !message.trim()}
|
||||
onClick={(e) => handleSubmit(e, message)}
|
||||
style={{
|
||||
backgroundColor: (!chatDisabled && !loading && message.trim()) ? "#ff8544" : "rgba(255, 255, 255, 0.1)",
|
||||
color: "white",
|
||||
margin: "2px",
|
||||
padding: "8px",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease"
|
||||
}}
|
||||
>
|
||||
<SendIcon color="primary" />
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
</form>
|
||||
{isMobile ? null :
|
||||
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 0,}} >
|
||||
{`The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`}
|
||||
|
||||
{!isMobile ? (
|
||||
<Typography variant="caption" style={{
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
textAlign: "center",
|
||||
display: "block",
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: 1.4,
|
||||
maxWidth: "80%",
|
||||
margin: "0 auto"
|
||||
}}>
|
||||
{messages.length === 0
|
||||
? `The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`
|
||||
: "Shuffle AI can make mistakes. Always double-check important information."
|
||||
}
|
||||
</Typography>
|
||||
}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ const CloudSyncTab = (props) => {
|
||||
const itemColor = "white";
|
||||
const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io";
|
||||
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const { themeMode, brandColor, setUpdateOrg } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
useEffect(() => { getSettings(); }, []);
|
||||
@@ -327,7 +327,6 @@ const CloudSyncTab = (props) => {
|
||||
);
|
||||
};
|
||||
const handleGetOrg = (orgId) => {
|
||||
|
||||
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
@@ -337,11 +336,16 @@ const CloudSyncTab = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (orgId === undefined || orgId === null || (orgId.length !== 36 && orgId.length !== 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (orgId.length === 0) {
|
||||
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Just use this one?
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||
@@ -359,7 +363,7 @@ const CloudSyncTab = (props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed getting your org. If this persists, please contact support.");
|
||||
toast.warn("Failed getting your org. If this persists, please contact support (cloud sync)")
|
||||
} else {
|
||||
if (
|
||||
responseJson.sync_features === undefined ||
|
||||
@@ -457,6 +461,8 @@ const CloudSyncTab = (props) => {
|
||||
setLoading(false);
|
||||
if (response.status === 200) {
|
||||
console.log("Cloud sync success?");
|
||||
handleGetOrg(userdata.active_org.id);
|
||||
setUpdateOrg(true);
|
||||
} else {
|
||||
console.log("Cloud sync fail?");
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const CreatorGrid = props => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => {
|
||||
-e SHUFFLE_SWARM_CONFIG=run \\
|
||||
-e BASE_URL="${newUrl}" \\${addProxy ? `
|
||||
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
||||
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
|
||||
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
|
||||
-v /tmp:/tmp \\` : ""}
|
||||
ghcr.io/shuffle/shuffle-orborus:latest
|
||||
`)
|
||||
@@ -467,8 +467,7 @@ 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 \\` : ""}
|
||||
ghcr.io/shuffle/shuffle-orborus:latest`
|
||||
|
||||
return commandData
|
||||
@@ -959,7 +958,7 @@ const EnvironmentTab = memo((props) => {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
|
||||
width: "100%",
|
||||
minWidth: 800,
|
||||
minWidth: showLoader ? 800 : 0,
|
||||
paddingBottom: 0,
|
||||
borderBottom: theme.palette.defaultBorder,
|
||||
}}
|
||||
@@ -976,7 +975,7 @@ const EnvironmentTab = memo((props) => {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
fontWeight: "bold",
|
||||
textAlign: header === "Actions" ? "left" : header === "Distribution" ? "right" : "center",
|
||||
textAlign: "center",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -987,7 +986,6 @@ const EnvironmentTab = memo((props) => {
|
||||
<ListItem
|
||||
key={rowIndex}
|
||||
style={{
|
||||
display: "grid",
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
height: 40,
|
||||
width: "100%",
|
||||
@@ -1674,10 +1672,9 @@ const EnvironmentTab = memo((props) => {
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
|
||||
<Typography variant='body2' color="textSecondary">Disable Detection Controller</Typography>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||
<Checkbox
|
||||
id="shuffle_skip_pipelines"
|
||||
@@ -1692,9 +1689,8 @@ const EnvironmentTab = memo((props) => {
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
|
||||
<Typography variant='body2' color="textSecondary">Use Remote Pipeline</Typography>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||
@@ -1706,16 +1702,16 @@ const EnvironmentTab = memo((props) => {
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentEnvQueue.length === 0 ? null :
|
||||
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
|
||||
{currentEnvQueue.map((queueItem, queueIndex) => {
|
||||
return (
|
||||
<ListItem
|
||||
key={queueIndex}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
border: '1px solid #333333',
|
||||
borderBottom: theme.palette.defaultBorder,
|
||||
maxHeight: 50,
|
||||
}}
|
||||
>
|
||||
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
|
||||
@@ -1767,7 +1763,7 @@ const EnvironmentTab = memo((props) => {
|
||||
padding: 15,
|
||||
textAlign: "center",
|
||||
height: 70,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as Pa
|
||||
import theme from '../theme.jsx';
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { toast } from 'react-toastify';
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
import { validateJson } from "../views/Workflows2.jsx";
|
||||
// import HandleJsonCopy from "./ShuffleCodeEditor1";
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
|
||||
@@ -64,7 +64,7 @@ const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
|
||||
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context);
|
||||
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, updateOrg, setUpdateOrg, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context);
|
||||
const [expandLeftNav, setExpandLeftNav] = useState(false);
|
||||
const [activeOrgName, setActiveOrgName] = useState(
|
||||
userdata?.active_org?.name || "Select Organziation"
|
||||
@@ -684,7 +684,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
|
||||
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
|
||||
Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer">
|
||||
2.1.1
|
||||
2.1.2
|
||||
</a>
|
||||
</Typography>
|
||||
</Menu>
|
||||
@@ -956,6 +956,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
.then((org) => {
|
||||
if (!fetched && org) {
|
||||
setActiveOrgData(org);
|
||||
setUpdateOrg(false);
|
||||
if (!isCloud) {
|
||||
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||
setIsProdStatusOn(true);
|
||||
@@ -972,7 +973,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
return () => {
|
||||
fetched = true;
|
||||
};
|
||||
}, [userdata?.active_org?.id, globalUrl]);
|
||||
}, [userdata?.active_org?.id, globalUrl, updateOrg]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1226,8 +1227,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to={"/new-dashboard"}
|
||||
to={userdata?.support === true ? "/new-dashboard" : "/workflows"}
|
||||
onClick={(event) => {
|
||||
if(!userdata?.support){
|
||||
return;
|
||||
}
|
||||
setOpenautomateTab(true);
|
||||
setOpenSecurityTab(false);
|
||||
setCurrentOpenTab("new-dashboard");
|
||||
|
||||
@@ -35,6 +35,7 @@ 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";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
// Helper function to get the correct image path based on app category
|
||||
@@ -1034,7 +1035,12 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
{/* Add Usecase Dialog */}
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
onClose={(event, reason) => {
|
||||
if (reason !== "backdropClick") {
|
||||
handleCloseDialog();
|
||||
}
|
||||
}}
|
||||
disableEscapeKeyDown
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
@@ -1642,7 +1648,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
<Box key={contentIndex} sx={{ mb: 1 }}>
|
||||
<TextField
|
||||
multiline
|
||||
rows={5}
|
||||
rows={6}
|
||||
value={content}
|
||||
onChange={(e) => {
|
||||
const newItems = [...formData.navigation.items];
|
||||
@@ -1653,7 +1659,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
navigation: { items: newItems },
|
||||
});
|
||||
}}
|
||||
placeholder="Content"
|
||||
placeholder="Content (Markdown supported)"
|
||||
helperText={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
|
||||
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
|
||||
<Typography variant="caption" sx={{ opacity: 0.7 }}>
|
||||
Markdown syntax is supported
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
fullWidth
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
@@ -1661,6 +1675,10 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
color: theme.palette.text.primary,
|
||||
"& fieldset": { border: theme.palette.defaultBorder },
|
||||
},
|
||||
"& .MuiOutlinedInput-root textarea": {
|
||||
overflow: "auto",
|
||||
resize: "vertical",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{item.content.length > 1 && (
|
||||
|
||||
@@ -104,13 +104,12 @@ const RunDetectionTest = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (haveDetectionPipelines() === false) {
|
||||
if (haveDetectionPipelines().length === 0) {
|
||||
setDetectionTestRunning(false)
|
||||
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 1. Run a new pipeline which exits.
|
||||
const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import`
|
||||
|
||||
@@ -124,7 +123,17 @@ const RunDetectionTest = (props) => {
|
||||
// 1. Submit it to run
|
||||
// 2. Check executions if they happened recently~
|
||||
if (submitPipelineWrapper !== undefined) {
|
||||
submitPipelineWrapper(detectionTest)
|
||||
|
||||
//`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
|
||||
const validEnv = haveDetectionPipelines()
|
||||
for (var envKey in validEnv) {
|
||||
const envName = validEnv[envKey]
|
||||
submitPipelineWrapper(detectionTest, envName)
|
||||
}
|
||||
|
||||
//if (validEnv.length > 0) {
|
||||
//toast.success(`Submitted detection test pipeline to environment(s): ${validEnv.join(", ")}`)
|
||||
//}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -141,10 +150,10 @@ const RunDetectionTest = (props) => {
|
||||
const haveDetectionPipelines = () => {
|
||||
if (pipelines === undefined) {
|
||||
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
|
||||
return false
|
||||
return []
|
||||
}
|
||||
|
||||
var foundCorrect = 0
|
||||
var foundCorrect = []
|
||||
for (var pipelineKey in pipelines) {
|
||||
const curPipeline = pipelines[pipelineKey]
|
||||
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
|
||||
@@ -152,27 +161,25 @@ const RunDetectionTest = (props) => {
|
||||
//}
|
||||
|
||||
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
|
||||
foundCorrect += 1
|
||||
foundCorrect.push(curPipeline.environment)
|
||||
}
|
||||
}
|
||||
|
||||
if (foundCorrect >= 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return foundCorrect
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", }}>
|
||||
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
|
||||
<Tooltip title={"Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline"} style={{}} aria-label={"Run detection test"}>
|
||||
<Tooltip title={
|
||||
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
|
||||
} style={{}} aria-label={"Run detection test"}>
|
||||
<div>
|
||||
<Button
|
||||
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
|
||||
disabled={haveDetectionPipelines().length === 0 || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
|
||||
onClick={() => {
|
||||
//setPipelineModalOpen(true)
|
||||
runDetectionTest()
|
||||
|
||||
@@ -13,8 +13,51 @@ function formatCompactNumber(value) {
|
||||
return `${n}`;
|
||||
}
|
||||
|
||||
const RunsOverTimeWidget = (props) => {
|
||||
const { globalUrl, onLoadingChange, monthOverride, dummyMode } = props;
|
||||
// Helpers to keep date and "today" logic concise and consistent
|
||||
function toYMD(date) {
|
||||
const d = new Date(date);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${dd}`;
|
||||
}
|
||||
|
||||
function entriesContainDateYMD(entries, date) {
|
||||
const target = toYMD(date);
|
||||
return Array.isArray(entries) && entries.some((e) => toYMD(e.date) === target);
|
||||
}
|
||||
|
||||
function computeTodayValueForAll(key, data) {
|
||||
if (key === 'app_executions') {
|
||||
return Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
|
||||
}
|
||||
if (key === 'workflow_executions') {
|
||||
const parent = Number(data?.daily_workflow_executions ?? 0);
|
||||
const parentFinished = Number(data?.daily_workflow_executions_finished ?? 0);
|
||||
const parentFailed = Number(data?.daily_workflow_executions_failed ?? 0);
|
||||
const childFinished = Number(data?.daily_child_workflow_executions_finished ?? 0);
|
||||
const childFailed = Number(data?.daily_child_workflow_executions_failed ?? 0);
|
||||
const parentVal = parent > 0 ? parent : (parentFinished + parentFailed);
|
||||
return parentVal + childFinished + childFailed;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function computeTodayValueForOrg(key, orgStats) {
|
||||
if (key === 'workflow_executions') {
|
||||
const total = orgStats?.daily_workflow_executions;
|
||||
const finished = Number(orgStats?.daily_workflow_executions_finished ?? 0);
|
||||
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
|
||||
return total !== undefined ? Number(total || 0) : (finished + failed);
|
||||
}
|
||||
if (key === 'app_executions') {
|
||||
return Number(orgStats?.daily_app_executions ?? 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const RunsOverTimeWidget = (props) => {
|
||||
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
|
||||
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
||||
const [series, setSeries] = useState([]);
|
||||
const [days, setDays] = useState(365); // aggregate to last 12 months by default
|
||||
@@ -24,6 +67,99 @@ const RunsOverTimeWidget = (props) => {
|
||||
// Helper: fetch time series for a specific statistics key
|
||||
const fetchSeriesForKey = async (key) => {
|
||||
try {
|
||||
// If specific org is selected and pre-fetched stats are available, use them directly
|
||||
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
||||
const dailyStats = orgStats.daily_statistics;
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (!day?.date) continue;
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue;
|
||||
|
||||
let value = 0;
|
||||
if (key === 'workflow_executions') {
|
||||
const finished = Number(day?.workflow_executions_finished || 0);
|
||||
const failed = Number(day?.workflow_executions_failed || 0);
|
||||
value = finished + failed;
|
||||
} else {
|
||||
value = Number(day?.[key] || 0);
|
||||
}
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
|
||||
// Append today's values if not present
|
||||
try {
|
||||
if (!entriesContainDateYMD(processedEntries, new Date())) {
|
||||
const todayVal = computeTodayValueForOrg(key, orgStats);
|
||||
if (!Number.isNaN(todayVal)) {
|
||||
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return processedEntries;
|
||||
}
|
||||
|
||||
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
|
||||
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
|
||||
// Use the full stats endpoint to get daily_statistics
|
||||
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
|
||||
const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
|
||||
|
||||
// Process each day's data, combining parent + child org runs
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (day?.date) {
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue; // Filter by days
|
||||
|
||||
let value = day?.[key] || 0;
|
||||
// Add child org executions based on the key
|
||||
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
|
||||
value += (day.child_app_executions || 0);
|
||||
} else if (key === 'workflow_executions') {
|
||||
// For workflow_executions, sum both child_workflow_executions_finished and child_workflow_executions_failed
|
||||
const childFinished = day?.child_workflow_executions_finished || 0;
|
||||
const childFailed = day?.child_workflow_executions_failed || 0;
|
||||
value += (childFinished + childFailed);
|
||||
}
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
}
|
||||
|
||||
// Append today's datapoint for ALL by combining parent + child daily_* counters
|
||||
try {
|
||||
if (!entriesContainDateYMD(processedEntries, new Date())) {
|
||||
const todayVal = computeTodayValueForAll(key, data);
|
||||
if (!Number.isNaN(todayVal)) {
|
||||
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return processedEntries;
|
||||
}
|
||||
}
|
||||
|
||||
// // If a specific org is selected
|
||||
// if (selectedOrgForStats && selectedOrgForStats !== 'ALL') {
|
||||
// const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
|
||||
// const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
|
||||
// if (r.ok) {
|
||||
// const j = await r.json();
|
||||
// return Array.isArray(j?.entries) ? j.entries : [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// Fallback to global stats
|
||||
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
|
||||
const doFetch = async (u) => {
|
||||
const r = await fetch(u, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
|
||||
@@ -34,23 +170,15 @@ const RunsOverTimeWidget = (props) => {
|
||||
const a = await doFetch(urlA);
|
||||
if (a.length > 0) return a;
|
||||
|
||||
// Optional org route fallback if present globally
|
||||
const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null;
|
||||
if (orgId) {
|
||||
const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
|
||||
const b = await doFetch(urlB);
|
||||
if (b.length > 0) return b;
|
||||
}
|
||||
|
||||
// Final fallback: old aggregate endpoint returning daily_statistics
|
||||
const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
|
||||
if (fallback.ok) {
|
||||
const data = await fallback.json();
|
||||
const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
|
||||
const valField = key;
|
||||
return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
|
||||
}
|
||||
return [];
|
||||
// // Final fallback: old aggregate endpoint returning daily_statistics
|
||||
// const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
|
||||
// if (fallback.ok) {
|
||||
// const data = await fallback.json();
|
||||
// const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
|
||||
// const valField = key;
|
||||
// return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
|
||||
// }
|
||||
// return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
@@ -154,9 +282,13 @@ const RunsOverTimeWidget = (props) => {
|
||||
}
|
||||
}, [monthOverride]);
|
||||
|
||||
useEffect(() => {
|
||||
load(mode);
|
||||
}, [mode, globalUrl, days, selectedMonth, dummyMode]);
|
||||
useEffect(() => {
|
||||
if (loadingSelectedOrgStats === true) {
|
||||
setSeries([]);
|
||||
return;
|
||||
}
|
||||
load(mode);
|
||||
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
|
||||
|
||||
// Notify parent on loading changes
|
||||
useEffect(() => {
|
||||
@@ -169,7 +301,7 @@ const RunsOverTimeWidget = (props) => {
|
||||
(Array.isArray(series) ? series : []).map((d) => {
|
||||
const dt = new Date(d.key);
|
||||
const label = selectedMonth instanceof Date
|
||||
? String(dt.getDate()) // day of month for daily view
|
||||
? `${dt.toLocaleString('default', { month: 'short' })} ${dt.getDate()}` // e.g., 'Oct 1'
|
||||
: dt.toLocaleString('default', { month: 'short' });
|
||||
return { key: label, data: Number(d?.data || 0) };
|
||||
})
|
||||
@@ -197,7 +329,7 @@ const RunsOverTimeWidget = (props) => {
|
||||
backgroundColor: "#1A1A1A",
|
||||
border: "1px solid rgba(255,255,255,0.22)",
|
||||
color: theme.palette.text.primary,
|
||||
padding: 8,
|
||||
padding: 10,
|
||||
maxWidth: 240,
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
@@ -208,6 +340,7 @@ const RunsOverTimeWidget = (props) => {
|
||||
/>
|
||||
}
|
||||
/>;
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -295,7 +428,12 @@ const RunsOverTimeWidget = (props) => {
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ width: '100%', position: 'relative' }}>
|
||||
{loadingSelectedOrgStats === true ? (
|
||||
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography variant="body2" color="textSecondary">Loading…</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<BarChart
|
||||
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
|
||||
height={300}
|
||||
@@ -315,6 +453,123 @@ const RunsOverTimeWidget = (props) => {
|
||||
}
|
||||
animated={false}
|
||||
/>
|
||||
)}
|
||||
{/* Custom reference line overlays for app execution limits (suborg and parent) */}
|
||||
{mode === 'apps' && barData.length > 0 && (() => {
|
||||
// Determine limits
|
||||
const parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
|
||||
const suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
|
||||
|
||||
const maxValue = Math.max(...barData.map(d => Number(d.data) || 0), 0);
|
||||
if (maxValue <= 0) return null;
|
||||
|
||||
const chartTopPadding = 1;
|
||||
const chartBottomPadding = 8;
|
||||
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
|
||||
|
||||
// Helpers to compute Y position in percentage
|
||||
const posFor = (limit) => {
|
||||
const perc = Math.max(0, Math.min(1, (Number(limit) || 0) / maxValue));
|
||||
return chartTopPadding + ((1 - perc) * usableHeight);
|
||||
};
|
||||
|
||||
let showParentLimit = false;
|
||||
let showSuborgLimit = false;
|
||||
|
||||
// Suborg/current limit: only when viewing a specific org (not ALL)
|
||||
if (selectedOrgForStats !== 'ALL' && typeof suborgAppExecutionLimit === 'number' && suborgAppExecutionLimit > 0 && suborgAppExecutionLimit <= maxValue && selectedOrgForStats === orgForLimit?.id) {
|
||||
showSuborgLimit = true;
|
||||
}
|
||||
|
||||
if (typeof parentAppExecutionLimit === 'number' && parentAppExecutionLimit > 0 && parentAppExecutionLimit <= maxValue) {
|
||||
if (selectedOrgForStats === 'ALL') {
|
||||
showParentLimit = true;
|
||||
} else if (parentAppExecutionLimit !== suborgAppExecutionLimit) {
|
||||
showParentLimit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!showParentLimit && !showSuborgLimit) return null;
|
||||
|
||||
const suborgTop = showSuborgLimit ? posFor(suborgAppExecutionLimit) : null;
|
||||
const parentTop = showParentLimit ? posFor(parentAppExecutionLimit) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSuborgLimit && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${suborgTop}%`,
|
||||
left: '2%',
|
||||
right: '0%',
|
||||
height: '2px',
|
||||
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${suborgTop}%`,
|
||||
right: '-5%',
|
||||
transform: 'translate(0, -50%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.95)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ff8544',
|
||||
}}
|
||||
>
|
||||
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}>
|
||||
{formatCompactNumber(suborgAppExecutionLimit)} limit
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showParentLimit && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${parentTop}%`,
|
||||
left: '2%',
|
||||
right: '0%',
|
||||
height: '2px',
|
||||
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${parentTop}%`,
|
||||
right: '-5%',
|
||||
transform: 'translate(0, -50%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9,
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.93)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #FD4C62',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}>
|
||||
{formatCompactNumber(parentAppExecutionLimit)} limit
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -768,6 +768,7 @@ const RuntimeDebugger = (props) => {
|
||||
toast("Error executing workflow: "+responseJson.error)
|
||||
} else {
|
||||
console.log("Executed workflow: ", responseJson)
|
||||
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -1236,8 +1237,8 @@ const RuntimeDebugger = (props) => {
|
||||
setRowsPerPage(newPaginationModel.pageSize)
|
||||
// No API call needed - this is just for client-side pagination
|
||||
}}
|
||||
|
||||
onRowSelectionModelChange={(newSelection) => {
|
||||
selectionModel={selectedWorkflowExecutions.map((workflow) => workflow.id)}
|
||||
onSelectionModelChange={(newSelection) => {
|
||||
//console.log("newSelection: ", newSelection)
|
||||
//setSelectedWorkflowExecutionsIndexes(newSelection)
|
||||
var found = []
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
TextField,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
@@ -39,11 +41,14 @@ const SchedulesTab = memo((props) => {
|
||||
const [showLoader, setShowLoader] = React.useState(true);
|
||||
const [workflows, setWorkflows] = React.useState([]);
|
||||
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
|
||||
const [newPipelineValue, setNewPipelineValue] = React.useState(`export | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
|
||||
const [newPipelineValue, setNewPipelineValue] = React.useState(`export live=true | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
|
||||
|
||||
const [ticketWebhook, setTicketWebhook] = React.useState("");
|
||||
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
|
||||
|
||||
const [environments, setEnvironments] = React.useState([]);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = React.useState("");
|
||||
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
@@ -74,7 +79,7 @@ const SchedulesTab = memo((props) => {
|
||||
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
|
||||
setDetectionWorkflowId(responseJson[i].id)
|
||||
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +96,7 @@ const SchedulesTab = memo((props) => {
|
||||
handleGetWorkflows()
|
||||
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
|
||||
handleGetAllTriggers()
|
||||
handleGetEnvironments()
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -168,8 +174,8 @@ const SchedulesTab = memo((props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const submitPipelineWrapper = (pipelineValue) => {
|
||||
submitPipeline(pipelineValue)
|
||||
const submitPipelineWrapper = (pipelineValue, environment) => {
|
||||
submitPipeline(pipelineValue, environment)
|
||||
}
|
||||
|
||||
const NewPipelineView = (
|
||||
@@ -277,9 +283,48 @@ const SchedulesTab = memo((props) => {
|
||||
setNewPipelineValue(event.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions style={{padding: "0px 50px 50px 50px", }}>
|
||||
{environments.length === 0 ? null :
|
||||
<div>
|
||||
<Typography variant="body1" color="textSecondary">Runtime Location</Typography>
|
||||
<Select
|
||||
label="Runtime Location"
|
||||
value={selectedEnvironment}
|
||||
onChange={(event) => {
|
||||
const selectedEnv = event.target.value
|
||||
setSelectedEnvironment(selectedEnv)
|
||||
}}
|
||||
style={{marginRight: 300, }}
|
||||
>
|
||||
{environments.map((environment, index) => {
|
||||
if (environment.archived) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (environment.Name === "Cloud") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
value={environment}
|
||||
>
|
||||
{environment.Name}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Button
|
||||
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
|
||||
onClick={() => {
|
||||
@@ -314,6 +359,10 @@ const SchedulesTab = memo((props) => {
|
||||
start_node: "",
|
||||
}
|
||||
|
||||
if (selectedEnvironment !== undefined && selectedEnvironment !== "") {
|
||||
pipelineConfig.environment = selectedEnvironment.Name
|
||||
}
|
||||
|
||||
if (environment !== undefined && environment !== "") {
|
||||
pipelineConfig.environment = environment
|
||||
}
|
||||
@@ -441,7 +490,46 @@ const SchedulesTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetEnvironments = () => {
|
||||
fetch(`${globalUrl}/api/v1/environments`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for getting all triggers");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setEnvironments(responseJson || []);
|
||||
|
||||
if (responseJson?.length > 0) {
|
||||
var selectedEnv = ""
|
||||
for (var i = 0; i < responseJson?.length; i++) {
|
||||
const env = responseJson[i]
|
||||
if (env.archived) {
|
||||
continue
|
||||
}
|
||||
|
||||
selectedEnv = env.Name
|
||||
if (env?.data_lake?.enabled === true) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedEnvironment(selectedEnv.Name)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// toast(error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const handleGetAllTriggers = () => {
|
||||
fetch(globalUrl + "/api/v1/triggers", {
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
|
||||
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
import { validateJson } from "../views/Workflows2.jsx";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
|
||||
@@ -969,6 +969,10 @@ const CodeEditor = (props) => {
|
||||
// Whelp this is inefficient af. Single loop pls
|
||||
// When the found array is empty.
|
||||
if (found !== null && found !== undefined) {
|
||||
// Resolve the child paths/variables first,
|
||||
// So that "$a.b.c" is handled before "$a.b" and then "$a"
|
||||
// Replace "$parent.child" before "$parent" so "$parent.child" doesn't become "parentValue.child" (which can't be matched later).
|
||||
found.sort((a, b) => b.length - a.length)
|
||||
|
||||
//console.log("FOUND: ", found)
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
LinearYAxisTickSeries,
|
||||
LinearYAxisTickLabel,
|
||||
} from "reaviz";
|
||||
import theme from "../theme";
|
||||
import theme from "../theme.jsx";
|
||||
|
||||
// KPI configuration constants
|
||||
const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run
|
||||
@@ -48,7 +48,11 @@ const statusOptions = [
|
||||
// This is used to format the date in the format of YYYY-MM-DD
|
||||
function formatDay(dateInput) {
|
||||
try {
|
||||
return new Date(dateInput).toISOString().slice(0, 10);
|
||||
const dt = new Date(dateInput);
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(dt.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`; // local date to avoid UTC shift dropping "today"
|
||||
} catch {
|
||||
return String(dateInput);
|
||||
}
|
||||
@@ -221,8 +225,8 @@ function buildGroupedSeries(selectedStatus, okArr, failArr) {
|
||||
return { grouped, scheme };
|
||||
}
|
||||
|
||||
const SuccessFailedRunsWidget = (props) => {
|
||||
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode } = props;
|
||||
const SuccessFailedRunsWidget = (props) => {
|
||||
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode, loadingSelectedOrgStats, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit } = props;
|
||||
|
||||
const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps'
|
||||
const [days, setDays] = useState(30);
|
||||
@@ -236,6 +240,25 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wfTotals, setWfTotals] = useState({ ok: 0, fail: 0, activeDays: 0 });
|
||||
|
||||
|
||||
const appExecutionLimit = useMemo(() => {
|
||||
if (mode !== 'apps') return null;
|
||||
|
||||
if(orgForLimit?.id !== selectedOrgForStats && selectedOrgForStats !== "ALL") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
let org = orgForLimit
|
||||
if(selectedOrgForStats === "ALL") {
|
||||
org = selectedOrganization;
|
||||
}
|
||||
if (!org?.sync_features?.app_executions?.limit) return null;
|
||||
return Number(org.sync_features.app_executions.limit) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [mode, selectedOrganization, orgForLimit, selectedOrgForStats]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof onTotalsChange !== "function") return;
|
||||
@@ -264,7 +287,132 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
|
||||
const fetchSeriesForKey = async (key) => {
|
||||
try {
|
||||
// If specific org is selected and pre-fetched stats are available, use them directly
|
||||
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
||||
const dailyStats = orgStats.daily_statistics;
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (!day?.date) continue;
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue;
|
||||
|
||||
let value = 0;
|
||||
if (key === 'workflow_executions') {
|
||||
const finished = Number(day?.workflow_executions_finished || 0);
|
||||
const failed = Number(day?.workflow_executions_failed || 0);
|
||||
value = finished + failed;
|
||||
} else {
|
||||
value = Number(day?.[key] || 0);
|
||||
}
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
|
||||
// Append today's numbers if missing from daily_statistics, using top-level daily_* fields
|
||||
try {
|
||||
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
|
||||
const hasToday = processedEntries.some(e => {
|
||||
const dt = new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}` === todayStr;
|
||||
});
|
||||
if (!hasToday) {
|
||||
let todayVal = 0;
|
||||
if (key === 'workflow_executions') {
|
||||
const finished = Number(orgStats?.daily_workflow_executions_finished ?? orgStats?.daily_workflow_executions ?? 0);
|
||||
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
|
||||
// If daily_workflow_executions exists, prefer that; otherwise sum finished + failed
|
||||
todayVal = orgStats?.daily_workflow_executions !== undefined ? Number(orgStats.daily_workflow_executions || 0) : (finished + failed);
|
||||
} else if (key === 'workflow_executions_finished') {
|
||||
todayVal = Number(orgStats?.daily_workflow_executions_finished ?? 0);
|
||||
} else if (key === 'workflow_executions_failed') {
|
||||
todayVal = Number(orgStats?.daily_workflow_executions_failed ?? 0);
|
||||
} else if (key === 'app_executions') {
|
||||
todayVal = Number(orgStats?.daily_app_executions ?? 0);
|
||||
} else if (key === 'app_executions_failed') {
|
||||
todayVal = Number(orgStats?.daily_app_executions_failed ?? 0);
|
||||
}
|
||||
|
||||
// Only add when we have a numeric value (including 0)
|
||||
if (!Number.isNaN(todayVal)) {
|
||||
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return processedEntries;
|
||||
}
|
||||
|
||||
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
|
||||
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
|
||||
// Use the full stats endpoint to get daily_statistics
|
||||
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
|
||||
const r = await fetch(url, { method: "GET", credentials: "include" });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
|
||||
|
||||
// Process each day's data, combining parent + child org runs
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (day?.date) {
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue; // Filter by days
|
||||
|
||||
let value = day?.[key] || 0;
|
||||
// Add child org executions based on the key
|
||||
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
|
||||
value += (day.child_app_executions || 0);
|
||||
} else if (key === 'app_executions_failed' && day?.child_app_executions_failed !== undefined) {
|
||||
value += (day.child_app_executions_failed || 0);
|
||||
} else if (key === 'workflow_executions_finished' && day?.child_workflow_executions_finished !== undefined) {
|
||||
value += (day.child_workflow_executions_finished || 0);
|
||||
} else if (key === 'workflow_executions_failed' && day?.child_workflow_executions_failed !== undefined) {
|
||||
value += (day.child_workflow_executions_failed || 0);
|
||||
} else if (key === 'workflow_executions') {
|
||||
// workflow_executions is total (finished + failed), so add both child fields
|
||||
const childFinished = day?.child_workflow_executions_finished || 0;
|
||||
const childFailed = day?.child_workflow_executions_failed || 0;
|
||||
value += (childFinished + childFailed);
|
||||
}
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
}
|
||||
|
||||
// Append today's datapoint for ALL by combining parent + child daily_* counters
|
||||
try {
|
||||
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
|
||||
const hasToday = processedEntries.some(e => { const dt=new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`===todayStr; });
|
||||
if (!hasToday) {
|
||||
let todayVal = 0;
|
||||
if (key === 'app_executions') {
|
||||
todayVal = Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
|
||||
} else if (key === 'app_executions_failed') {
|
||||
todayVal = Number(data?.daily_app_executions_failed ?? 0) + Number(data?.daily_child_app_executions_failed ?? 0);
|
||||
} else if (key === 'workflow_executions_finished') {
|
||||
todayVal = Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_child_workflow_executions_finished ?? 0);
|
||||
} else if (key === 'workflow_executions_failed') {
|
||||
todayVal = Number(data?.daily_workflow_executions_failed ?? 0) + Number(data?.daily_child_workflow_executions_failed ?? 0);
|
||||
} else if (key === 'workflow_executions') {
|
||||
const p = Number(data?.daily_workflow_executions ?? 0);
|
||||
const cf = Number(data?.daily_child_workflow_executions_finished ?? 0);
|
||||
const cfa = Number(data?.daily_child_workflow_executions_failed ?? 0);
|
||||
todayVal = p > 0 ? p : (Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_workflow_executions_failed ?? 0));
|
||||
todayVal += (cf + cfa);
|
||||
}
|
||||
if (!Number.isNaN(todayVal)) {
|
||||
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return processedEntries;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to global stats
|
||||
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
|
||||
const doFetch = async (u) => {
|
||||
const r = await fetch(u, { method: "GET", credentials: "include" });
|
||||
@@ -272,25 +420,17 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
const j = await r.json();
|
||||
return Array.isArray(j?.entries) ? j.entries : [];
|
||||
};
|
||||
const a = await doFetch(urlA);
|
||||
if (a.length > 0) return a;
|
||||
|
||||
// Optional orgId fallback if exposed globally in app
|
||||
// const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null;
|
||||
// if (orgId) {
|
||||
// const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
|
||||
// const b = await doFetch(urlB);
|
||||
// if (b.length > 0) return b;
|
||||
// }
|
||||
// return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const a = await doFetch(urlA);
|
||||
return a;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSeries = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// This will only run if dummyMode is true (Onboarding preview)
|
||||
if (dummyMode) {
|
||||
// 10-day wave with a couple of bumps for a more dynamic preview
|
||||
const today = new Date();
|
||||
@@ -324,47 +464,6 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
let okSeries = normalizeEntries(succ);
|
||||
let failSeries = normalizeEntries(fail);
|
||||
|
||||
// Fallback: if empty, derive from /api/v1/stats daily_statistics
|
||||
if (okSeries.length === 0 && failSeries.length === 0) {
|
||||
const resp = await fetch(`${globalUrl}/api/v1/stats`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const fieldOk =
|
||||
mode === "workflows"
|
||||
? "workflow_executions_finished"
|
||||
: "app_executions";
|
||||
const fieldFail =
|
||||
mode === "workflows"
|
||||
? "workflow_executions_failed"
|
||||
: "app_executions_failed";
|
||||
const list = Array.isArray(data?.daily_statistics)
|
||||
? data.daily_statistics
|
||||
: [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
okSeries = list
|
||||
.filter(Boolean)
|
||||
.map((d) => ({
|
||||
key: new Date(d?.date || Date.now()),
|
||||
id: d?.date || Math.random().toString(36).slice(2),
|
||||
data: Number(d?.[fieldOk] || 0),
|
||||
}))
|
||||
.filter((p) => p.key >= cutoff);
|
||||
failSeries = list
|
||||
.filter(Boolean)
|
||||
.map((d) => ({
|
||||
key: new Date(d?.date || Date.now()),
|
||||
id: `f-${d?.date || Math.random().toString(36).slice(2)}`,
|
||||
data: Number(d?.[fieldFail] || 0),
|
||||
}))
|
||||
.filter((p) => p.key >= cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
setSeriesOk(okSeries);
|
||||
setSeriesFail(failSeries);
|
||||
} catch (e) {
|
||||
@@ -375,16 +474,15 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Only fetch on first render and when days window or mode changes
|
||||
const firstLoadRef = React.useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstLoadRef.current) {
|
||||
firstLoadRef.current = false;
|
||||
if (loadingSelectedOrgStats === true) {
|
||||
setSeriesOk([]);
|
||||
setSeriesFail([]);
|
||||
return;
|
||||
}
|
||||
fetchSeries();
|
||||
return;
|
||||
}
|
||||
fetchSeries();
|
||||
}, [dummyMode, days, globalUrl, mode]);
|
||||
}, [dummyMode, days, globalUrl, mode, selectedOrgForStats, selectedOrganization, loadingSelectedOrgStats]);
|
||||
|
||||
// Apply external days override (e.g. after onboarding completes)
|
||||
useEffect(() => {
|
||||
@@ -406,7 +504,13 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
const totalEntries = await fetchSeriesForKey("workflow_executions");
|
||||
const series = normalizeEntries(totalEntries);
|
||||
const dayKey = (d) => {
|
||||
try { return new Date(d?.date || d?.key).toISOString().slice(0,10); } catch { return null; }
|
||||
try {
|
||||
const dt = new Date(d?.date || d?.key);
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(dt.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`; // local date string
|
||||
} catch { return null; }
|
||||
};
|
||||
const dayTotals = new Map();
|
||||
for (const it of series) {
|
||||
@@ -421,14 +525,14 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
};
|
||||
run();
|
||||
return () => { aborted = true; };
|
||||
}, [globalUrl, days, dummyMode, overrideDays]);
|
||||
}, [globalUrl, days, dummyMode, overrideDays, selectedOrgForStats]);
|
||||
|
||||
// Notify parent about loading state changes
|
||||
useEffect(() => {
|
||||
if (typeof onLoadingChange === "function") {
|
||||
onLoadingChange(loading);
|
||||
onLoadingChange(loadingSelectedOrgStats || loading);
|
||||
}
|
||||
}, [loading, onLoadingChange]);
|
||||
}, [loading, loadingSelectedOrgStats, onLoadingChange]);
|
||||
|
||||
// Build filters UI once here; optionally render externally via onControlsChange
|
||||
const controlsNode = React.useMemo(() => (
|
||||
@@ -600,6 +704,9 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
}
|
||||
}, [onControlsChange, controlsNode]);
|
||||
|
||||
var showParentLimit = false;
|
||||
var showSuborgLimit = false;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{/* Top controls row: title left, all filters on the right */}
|
||||
@@ -637,7 +744,11 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
}}
|
||||
>
|
||||
<Typography style={{ fontWeight: 500, marginBottom: 22, fontSize: 18 }}>Successful vs Failed Runs ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
|
||||
{(() => {
|
||||
{loadingSelectedOrgStats === true ? (
|
||||
<div style={{ height: 300, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<Typography variant="body2" color="textSecondary">Loading…</Typography>
|
||||
</div>
|
||||
) : (() => {
|
||||
// Build unified timeline by day or by month
|
||||
const useOk = Array.isArray(seriesOk) ? seriesOk : [];
|
||||
const useFail = Array.isArray(seriesFail) ? seriesFail : [];
|
||||
@@ -690,39 +801,60 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
);
|
||||
const paddedMax = computePaddedMax(okArr, failArr);
|
||||
|
||||
// Extract parent and suborg app execution limits
|
||||
let parentAppExecutionLimit = null;
|
||||
let suborgAppExecutionLimit = null;
|
||||
if (mode === 'apps') {
|
||||
parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
|
||||
suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
|
||||
|
||||
console.log("Mode appExecutionLimit found in sub orgs parentAppExecutionLimit", parentAppExecutionLimit);
|
||||
console.log("Mode appExecutionLimit found in sub orgs suborgAppExecutionLimit", suborgAppExecutionLimit);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof appExecutionLimit === 'number' &&
|
||||
appExecutionLimit > 0 &&
|
||||
appExecutionLimit <= paddedMax &&
|
||||
selectedOrgForStats !== "ALL"
|
||||
) {
|
||||
showSuborgLimit = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<AreaChart
|
||||
height={300}
|
||||
width={"100%"}
|
||||
data={grouped}
|
||||
yAxis={
|
||||
<LinearYAxis
|
||||
domain={[0, paddedMax]}
|
||||
tickSeries={
|
||||
<LinearYAxisTickSeries
|
||||
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
xAxis={
|
||||
<LinearXAxis
|
||||
tickSeries={
|
||||
<LinearXAxisTickSeries
|
||||
tickSize={0}
|
||||
label={
|
||||
<LinearXAxisTickLabel
|
||||
padding={8}
|
||||
format={(d) => formatLabel(Number(d))}
|
||||
/>
|
||||
}
|
||||
tickValues={tickValues}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
|
||||
series={
|
||||
<div style={{ position: 'relative', width: '100%', height: 300 }}>
|
||||
<AreaChart
|
||||
height={300}
|
||||
width={"100%"}
|
||||
data={grouped}
|
||||
yAxis={
|
||||
<LinearYAxis
|
||||
domain={[0, paddedMax]}
|
||||
tickSeries={
|
||||
<LinearYAxisTickSeries
|
||||
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
xAxis={
|
||||
<LinearXAxis
|
||||
tickSeries={
|
||||
<LinearXAxisTickSeries
|
||||
tickSize={0}
|
||||
label={
|
||||
<LinearXAxisTickLabel
|
||||
padding={8}
|
||||
format={(d) => formatLabel(Number(d))}
|
||||
/>
|
||||
}
|
||||
tickValues={tickValues}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
|
||||
series={
|
||||
<AreaSeries
|
||||
type="grouped"
|
||||
interpolation={"smooth"}
|
||||
@@ -822,6 +954,119 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Custom reference line overlay - only show if limit is within visible range */}
|
||||
{appExecutionLimit && mode === 'apps' && (() => {
|
||||
const maxValue = paddedMax || 1;
|
||||
const limitPercentage = appExecutionLimit / maxValue;
|
||||
const chartTopPadding = 1;
|
||||
const chartBottomPadding = 8;
|
||||
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
|
||||
const topPercentage = chartTopPadding + ((1 - limitPercentage) * usableHeight);
|
||||
|
||||
let parentLimitPos = null;
|
||||
if (
|
||||
typeof parentAppExecutionLimit === 'number' &&
|
||||
parentAppExecutionLimit > 0 &&
|
||||
parentAppExecutionLimit !== appExecutionLimit &&
|
||||
parentAppExecutionLimit <= paddedMax
|
||||
) {
|
||||
showParentLimit = true;
|
||||
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
|
||||
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
|
||||
}
|
||||
|
||||
|
||||
if (selectedOrgForStats === "ALL") {
|
||||
showParentLimit = true;
|
||||
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
|
||||
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Suborg or Current Limit Reference Line & Label (orange now) */}
|
||||
{
|
||||
selectedOrgForStats !== "ALL" && showSuborgLimit && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${topPercentage}%`,
|
||||
left: '3.9%',
|
||||
right: '0%',
|
||||
height: '2px',
|
||||
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${topPercentage}%`,
|
||||
right: '-8%',
|
||||
transform: 'translate(0, -50%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.95)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ff8544',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}
|
||||
>
|
||||
{formatCompactNumber(appExecutionLimit)} limit
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Parent Org Limit Reference Line & Label (red now) */}
|
||||
{showParentLimit && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${parentLimitPos}%`,
|
||||
left: '3.9%',
|
||||
right: '0%',
|
||||
height: '2px',
|
||||
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${parentLimitPos}%`,
|
||||
right: '-8%',
|
||||
transform: 'translate(0, -50%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9,
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.93)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #FD4C62',
|
||||
display: 'flex', alignItems: 'center', gap: 6
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}
|
||||
>
|
||||
{formatCompactNumber(parentAppExecutionLimit)} limit
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
@@ -844,8 +1089,19 @@ const SuccessFailedRunsWidget = (props) => {
|
||||
<LegendDot color="#22c55e" /> Successful Runs
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<LegendDot color="#ef4444" /> Failed Runs
|
||||
<LegendDot color="#FD4C62" /> Failed Runs
|
||||
</div>
|
||||
{/* Chart limit legends */}
|
||||
{showParentLimit && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 4, marginLeft: 18 }}>
|
||||
<LegendLineDash color="#FD4C62" height="10" /> <span>Parent Org Limit</span>
|
||||
</div>
|
||||
)}
|
||||
{showSuborgLimit && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<LegendLineDash color="#ff8544" height="10" /> <span>Current Org Limit</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -999,3 +1255,18 @@ function LegendDot({ color }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LegendLineDash({ color, height = 10 }) {
|
||||
// Render a small thinned dashed SVG line
|
||||
return (
|
||||
<svg width="24" height={height} style={{ margin: '0 2px' }}>
|
||||
<line
|
||||
x1="2" x2="32" y1={height / 2} y2={height / 2}
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeDasharray="8, 5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Zoom,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import theme from "../theme.jsx";
|
||||
const itemHeight = 24
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const AppContext = (props) => {
|
||||
const [windowWidth, setWindowWidth] = useState(serverside === true ? 100 : window.innerWidth);
|
||||
const [brandColor, setBrandColor] = useState(() => localStorage.getItem("brandColor") || "#ff8544");
|
||||
const [brandName, setBrandName] = useState(()=> localStorage.getItem("brandName") || "Shuffle");
|
||||
const [updateOrg, setUpdateOrg] = useState(false);
|
||||
|
||||
const [themeMode, setThemeMode] = useState(
|
||||
() => localStorage.getItem("theme") || "dark"
|
||||
@@ -117,6 +118,8 @@ export const AppContext = (props) => {
|
||||
setBrandColor,
|
||||
brandName,
|
||||
setBrandName,
|
||||
updateOrg,
|
||||
setUpdateOrg,
|
||||
}}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
|
||||
@@ -552,6 +552,37 @@ export default function defaultCytoscapeStyle(theme) {
|
||||
"font-size": "0px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `edge[?correlation]`,
|
||||
css: {
|
||||
"curve-style": "straight",
|
||||
"width": 2,
|
||||
"line-color": "#ccc",
|
||||
"target-arrow-color": "#ccc",
|
||||
"source-arrow-shape": "none",
|
||||
"target-arrow-shape": "none",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[type="CORRELATION"]`,
|
||||
css: {
|
||||
label: function (element) {
|
||||
return element.data("label")
|
||||
},
|
||||
shape: "round",
|
||||
color: "data(color)",
|
||||
width: "data(width)",
|
||||
height: "data(height)",
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
"background-color": "data(iconBackground)",
|
||||
"background-fill": "data(fillstyle)",
|
||||
"background-gradient-direction": "to-right",
|
||||
"background-gradient-stop-colors": "data(fillGradient)",
|
||||
"border-width": "0px",
|
||||
"font-size": "0px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[?source_workflow]`,
|
||||
css: {
|
||||
@@ -578,41 +609,41 @@ export default function defaultCytoscapeStyle(theme) {
|
||||
selector: `node[name="switch"]`,
|
||||
css: {
|
||||
label: function(element) {
|
||||
// Load from the actual element
|
||||
var nodeheight = 400
|
||||
var conditions = [{
|
||||
"name": "Condition 1",
|
||||
"check": "X equals Y",
|
||||
},
|
||||
{
|
||||
"name": "Condition 2",
|
||||
"check": "X2 equals Y2",
|
||||
},
|
||||
{
|
||||
"name": "Condition 3",
|
||||
"check": "X3 equals Y3",
|
||||
}]
|
||||
|
||||
conditions.push({
|
||||
"name": "Else",
|
||||
"check": "If all else fails",
|
||||
})
|
||||
|
||||
const newlines = nodeheight / conditions.length
|
||||
console.log("Newlines: ", newlines)
|
||||
|
||||
const label = conditions.map((condition) => {
|
||||
return `${condition.name}\n\n\n`
|
||||
}).join("\n")
|
||||
|
||||
return label
|
||||
},
|
||||
// Load from the actual element
|
||||
var nodeheight = 400
|
||||
var conditions = [{
|
||||
"name": "Condition 1",
|
||||
"check": "X equals Y",
|
||||
},
|
||||
{
|
||||
"name": "Condition 2",
|
||||
"check": "X2 equals Y2",
|
||||
},
|
||||
{
|
||||
"name": "Condition 3",
|
||||
"check": "X3 equals Y3",
|
||||
}]
|
||||
|
||||
conditions.push({
|
||||
"name": "Else",
|
||||
"check": "If all else fails",
|
||||
})
|
||||
|
||||
const newlines = nodeheight / conditions.length
|
||||
console.log("Newlines: ", newlines)
|
||||
|
||||
const label = conditions.map((condition) => {
|
||||
return `${condition.name}\n\n\n`
|
||||
}).join("\n")
|
||||
|
||||
return label
|
||||
},
|
||||
color: theme.palette.text.primary || "white",
|
||||
"border-color": "#f85a3e",
|
||||
"background-color": "#1f1f1f",
|
||||
"background-color": "#1f1f1f",
|
||||
"font-size": "19px",
|
||||
"text-margin-x": "-110px",
|
||||
"text-wrap": "wrap",
|
||||
"text-wrap": "wrap",
|
||||
shape: "roundrectangle",
|
||||
width: "100",
|
||||
height: "300",
|
||||
|
||||
@@ -1765,7 +1765,17 @@ If you're interested, please let me know a time that works for you, or set up a
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000);
|
||||
toast("Successfully changed active organization - refreshing!");
|
||||
|
||||
toast.success("Successfully changed active organization - refreshing!");
|
||||
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
|
||||
navigate(`/admin?org_id=${responseJson.org_id}`)
|
||||
} else {
|
||||
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
|
||||
navigate(`/admin`)
|
||||
} else {
|
||||
toast("No pivot?")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
|
||||
@@ -2,19 +2,23 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import AdminNavBar from '../components/AdminNavBar.jsx';
|
||||
import { toast } from "react-toastify";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
|
||||
|
||||
const Admin2 = (props) => {
|
||||
// Destructure props if needed
|
||||
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, isLoggedIn} = props;
|
||||
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, } = props;
|
||||
const [selectedTab, setSelectedTab] = useState('editdetails');
|
||||
const [selectedStatus, setSelectedStatus] = React.useState([]);
|
||||
const [selectedOrganization, setSelectedOrganization] = useState({});
|
||||
const [organizationFeatures, setOrganizationFeatures] = useState({});
|
||||
const [orgRequest, setOrgRequest] = React.useState(true);
|
||||
const [isOrgLoaded, setIsOrgLoaded] = React.useState(false);
|
||||
const {brandName} = useContext(Context)
|
||||
const {brandName, updateOrg, setUpdateOrg} = useContext(Context)
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
if (document !== undefined) {
|
||||
if (selectedOrganization?.name !== undefined) {
|
||||
document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`;
|
||||
@@ -24,6 +28,9 @@ const Admin2 = (props) => {
|
||||
}
|
||||
|
||||
const handleGetOrg = (orgId) => {
|
||||
if (orgId === undefined || orgId === null || orgId.length !== 36) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||
method: "GET",
|
||||
@@ -40,13 +47,13 @@ const Admin2 = (props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast(
|
||||
"Failed getting your org. If this persists, please contact support. Redirecting to workflows...",
|
||||
);
|
||||
toast.warn("Failed getting your org. If this persists, please contact support. Redirecting to workflows...")
|
||||
setTimeout(() => {
|
||||
window.location.href = "/workflows";
|
||||
}, 3000);
|
||||
} else {
|
||||
|
||||
setUpdateOrg(false);
|
||||
if (
|
||||
responseJson.sync_features === undefined ||
|
||||
responseJson.sync_features === null
|
||||
@@ -163,6 +170,13 @@ const Admin2 = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (updateOrg && userdata?.active_org?.id !== undefined && userdata?.active_org?.id !== null && userdata?.active_org?.id.length > 0) {
|
||||
handleGetOrg(userdata.active_org.id);
|
||||
setUpdateOrg(false);
|
||||
}
|
||||
|
||||
}, [updateOrg]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
@@ -216,16 +230,26 @@ const Admin2 = (props) => {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000);
|
||||
toast("Successfully changed active organization - refreshing!");
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
toast("Failed changing org: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Failed changing org")
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Successfully changed active organization - refreshing!");
|
||||
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
|
||||
navigate(`/admin?org_id=${responseJson.org_id}`)
|
||||
} else {
|
||||
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
|
||||
navigate(`/admin`)
|
||||
} else {
|
||||
toast("No pivot?")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
toast("Failed changing org: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Failed changing org")
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error changing: ", error);
|
||||
|
||||
+345
-67
@@ -6,7 +6,11 @@ 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 { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows2.jsx";
|
||||
import AppSearch from "../components/Appsearch.jsx";
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Paragraph, Blockquote, CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
|
||||
import {
|
||||
Box,
|
||||
@@ -18,6 +22,8 @@ import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
TextField,
|
||||
Popover,
|
||||
Divider,
|
||||
} from '@mui/material'
|
||||
|
||||
import {
|
||||
@@ -32,10 +38,13 @@ import {
|
||||
Close as CloseIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Add as AddIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import {
|
||||
green,
|
||||
yellow,
|
||||
red,
|
||||
} from '../views/AngularWorkflow.jsx'
|
||||
|
||||
@@ -55,11 +64,35 @@ const AgentUI = (props) => {
|
||||
const [actionInput, setActionInput] = useState("")
|
||||
const [questionAnswers, setQuestionAnswers] = useState({})
|
||||
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
||||
const [chosenApps, setChosenApps] = useState([])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
|
||||
return
|
||||
}
|
||||
|
||||
setNewSelectedApp({})
|
||||
setAppPickerAnchor(null)
|
||||
if (chosenApps.findIndex((app) => app.id === newSelectedApp.objectID) !== -1) {
|
||||
} else {
|
||||
setChosenApps(chosenApps.concat([{
|
||||
name: newSelectedApp.name,
|
||||
id: newSelectedApp.objectID,
|
||||
image: newSelectedApp.image_url,
|
||||
}]))
|
||||
}
|
||||
}, [newSelectedApp])
|
||||
|
||||
const {themeMode} = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const navigate = useNavigate();
|
||||
|
||||
document.title = "Shuffle AI Agents"
|
||||
if (document !== undefined && document !== null && !document?.title?.includes("Agent")) {
|
||||
document.title = "Shuffle AI Agents"
|
||||
}
|
||||
|
||||
const agentWrapperStyle = {
|
||||
width: 1000,
|
||||
@@ -79,6 +112,44 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const Heading = (props) => {
|
||||
const element = React.createElement(
|
||||
`h${props.level}`,
|
||||
{ style: { marginTop: 40 } },
|
||||
props.children
|
||||
);
|
||||
|
||||
return (
|
||||
<Typography>
|
||||
{props.level !== 1 ? (
|
||||
<Divider
|
||||
style={{
|
||||
width: "90%",
|
||||
marginTop: 40,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{element}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const markdownComponents = {
|
||||
img: Img,
|
||||
code: CodeHandler,
|
||||
h1: Heading,
|
||||
h2: Heading,
|
||||
h3: Heading,
|
||||
h4: Heading,
|
||||
h5: Heading,
|
||||
h6: Heading,
|
||||
a: OuterLink,
|
||||
p: Paragraph,
|
||||
blockquote: Blockquote,
|
||||
}
|
||||
|
||||
const findNodeData = (execution_data, node_id) => {
|
||||
if (execution_data === undefined || execution_data === null) {
|
||||
return
|
||||
@@ -311,7 +382,73 @@ const AgentUI = (props) => {
|
||||
getAppAuth()
|
||||
}, [])
|
||||
|
||||
const maxTimelineWidth = 300
|
||||
const maxTimelineWidth = 380
|
||||
|
||||
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
|
||||
console.log("Submitting questions: ", decisionId, questionAnswers)
|
||||
if (decisionId === undefined || decisionId === null || decisionId === "") {
|
||||
toast.error("No decision ID provided. Cannot submit answers.")
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(questionAnswers).length === 0) {
|
||||
toast.error("No answers provided. Cannot submit empty answers.")
|
||||
return
|
||||
}
|
||||
|
||||
var newArgument = {}
|
||||
if (isContinuation === true) {
|
||||
// Just a single answer
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
newArgument[key] = answer
|
||||
}
|
||||
|
||||
if (Object.keys(newArgument).length === 0) {
|
||||
toast.error("No answers details. Cannot submit the answer.")
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
if (isContinuation === true) {
|
||||
newArgument["question_"+(answer.index)] = answer.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAgentRequestLoading(true)
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const executionId = params.get("execution_id")
|
||||
const nodeId = params.get("node_id")
|
||||
const authorization = params.get("authorization")
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setAgentRequestLoading(false)
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success !== false) {
|
||||
setTimeout(() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}, 500)
|
||||
|
||||
toast.success("Successfully submitted answers! The agent should continue shortly.")
|
||||
} else {
|
||||
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setAgentRequestLoading(false)
|
||||
toast.error("Problem with submitting: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
var latestEndTime = 0
|
||||
var originalStartTime = 0
|
||||
@@ -332,6 +469,11 @@ const AgentUI = (props) => {
|
||||
<ErrorIcon style={{color: red, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
item.status === "IGNORED" || item.status === "IGNORE" ?
|
||||
<Tooltip title={`${item.status}: Previous FAILURE before the agent was reran.`} placement="top">
|
||||
<WarningIcon style={{color: yellow, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
|
||||
<HourglassDisabledIcon style={{marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
@@ -399,6 +541,10 @@ const AgentUI = (props) => {
|
||||
const defaultTopPadding = 10
|
||||
const open = openIndexes.includes(index)
|
||||
|
||||
if (item?.type === "agent" && item?.details?.original_input !== undefined) {
|
||||
document.title = "Agent: " + item?.details?.original_input?.substring(0, 50)
|
||||
}
|
||||
|
||||
var questions = []
|
||||
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||
item.type = "finalise"
|
||||
@@ -434,6 +580,10 @@ const AgentUI = (props) => {
|
||||
<Tooltip title="Ask" placement="top">
|
||||
<img src="/images/workflows/UserInput2.svg" style={categoryStyle} />
|
||||
</Tooltip>
|
||||
: item.category === "finalise" || item.category === "finish" || item.action === "finish" ?
|
||||
<Tooltip title="The action finished successfully" placement="top">
|
||||
<CheckIcon style={{color: green, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
<div style={categoryStyle} />
|
||||
|
||||
@@ -487,10 +637,10 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const barColor = item.status === "FINISHED" ? green :
|
||||
const barColor = item.status === "IGNORED" ? yellow : item.status === "FINISHED" ? green :
|
||||
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
||||
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
||||
theme.palette.surfaceColor
|
||||
red
|
||||
|
||||
const rerunAgentButton =
|
||||
<Tooltip title="Rerun from the start with the same input" placement="right">
|
||||
@@ -501,7 +651,7 @@ const AgentUI = (props) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.info("Attempting to rerun everything.")
|
||||
toast.info("Rerunning agent with the same input.")
|
||||
setDisableButtons(true)
|
||||
|
||||
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
|
||||
@@ -529,7 +679,7 @@ const AgentUI = (props) => {
|
||||
|
||||
|
||||
const rerunButton =
|
||||
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="right">
|
||||
<Tooltip title="Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
@@ -546,56 +696,7 @@ const AgentUI = (props) => {
|
||||
<RestartAltIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
const submitQuestions = (decisionId, questionAnswers) => {
|
||||
console.log("Submitting questions: ", decisionId, questionAnswers)
|
||||
if (decisionId === undefined || decisionId === null || decisionId === "") {
|
||||
toast.error("No decision ID provided. Cannot submit answers.")
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(questionAnswers).length === 0) {
|
||||
toast.error("No answers provided. Cannot submit empty answers.")
|
||||
return
|
||||
}
|
||||
|
||||
// Loop qu
|
||||
var newArgument = {}
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
newArgument["question_"+(answer.index)] = answer.value
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const executionId = params.get("execution_id")
|
||||
const nodeId = params.get("node_id")
|
||||
const authorization = params.get("authorization")
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
|
||||
console.log("PARSED URL: ", url)
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success !== false) {
|
||||
setTimeout(() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}, 500)
|
||||
|
||||
toast.success("Successfully submitted answers! The agent should continue shortly.")
|
||||
} else {
|
||||
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Problem with submitting: " + error)
|
||||
})
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -675,6 +776,7 @@ const AgentUI = (props) => {
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
paddingTop: defaultTopPadding,
|
||||
paddingBottom: defaultTopPadding,
|
||||
}}>
|
||||
{item.label}
|
||||
</div>
|
||||
@@ -746,10 +848,11 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
*/}
|
||||
|
||||
<Tooltip title="See in another window" placement="left">
|
||||
<Tooltip title="Answer in the Form UI" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
style={{marginLeft: 0, }}
|
||||
disabled={item?.details?.run_details?.status === "FINISHED"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -767,6 +870,28 @@ const AgentUI = (props) => {
|
||||
:
|
||||
item.category === "agent" ?
|
||||
rerunAgentButton
|
||||
:
|
||||
item?.type === "decision" ?
|
||||
<div style={{display: "flex", }}>
|
||||
{rerunButton}
|
||||
<Tooltip title="Explore/debug execution" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
|
||||
style={{marginLeft: 0, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
|
||||
window.open(item?.details?.run_details?.debug_url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon color={barColor === red ? "primary" : "secondary"} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
:
|
||||
rerunButton
|
||||
}
|
||||
@@ -805,13 +930,25 @@ const AgentUI = (props) => {
|
||||
{questions.map((q, questionIndex) => {
|
||||
return (
|
||||
<div style={{marginTop: 25, }}>
|
||||
<Typography variant="body2">
|
||||
{`${q.question}`}
|
||||
</Typography>
|
||||
<div id="markdown_wrapper_outer" style={{cursor: "default", }}>
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
className={"style.reactMarkdown"}
|
||||
escapeHtml={false}
|
||||
skipHtml={false}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
style={{
|
||||
maxWidth: "100%", minWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{q.question}
|
||||
</Markdown>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label={`Question ${q.index}`}
|
||||
placeholder="No question found"
|
||||
placeholder="Your answer here"
|
||||
variant="outlined"
|
||||
style={{width: 800, marginTop: 20, }}
|
||||
multiline
|
||||
@@ -838,7 +975,7 @@ const AgentUI = (props) => {
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{marginTop: 10, }}
|
||||
style={{marginTop: 16, }}
|
||||
disabled={questionSubmitDisabled}
|
||||
onClick={() => {
|
||||
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
||||
@@ -887,6 +1024,8 @@ const AgentUI = (props) => {
|
||||
const TimelineRender = (props) => {
|
||||
const { agent_data } = props;
|
||||
|
||||
const [continuationText, setContinuationText] = useState("")
|
||||
|
||||
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||
const validate = validateJson(actionResult?.result)
|
||||
if (validate.valid === true) {
|
||||
@@ -938,6 +1077,7 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
var finishDecisionId = ""
|
||||
var sortedTimelineItems = []
|
||||
for (var key in agent_data?.decisions) {
|
||||
const item = agent_data.decisions[key]
|
||||
@@ -962,6 +1102,11 @@ const AgentUI = (props) => {
|
||||
|
||||
newTimelineItem.details = item
|
||||
timelineItems.push(newTimelineItem)
|
||||
|
||||
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||
finishDecisionId = item?.run_details?.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
timelineItems.sort((a, b) => {
|
||||
@@ -1002,6 +1147,72 @@ const AgentUI = (props) => {
|
||||
)
|
||||
})}
|
||||
|
||||
{finishDecisionId !== "" ?
|
||||
<Box
|
||||
component="form"
|
||||
style={{width: "100%", textAlign: "center",}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Uses the submitQuestion and adds more details to
|
||||
//setAgentRequestLoading ?
|
||||
submitQuestions(finishDecisionId, {
|
||||
"continue": continuationText,
|
||||
}, true)
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 50, }}>
|
||||
<div>
|
||||
<TextField
|
||||
label="Add more details to the current task"
|
||||
variant="outlined"
|
||||
disabled={agentRequestLoading}
|
||||
style={{width: 400, margin: "auto", }}
|
||||
multiline
|
||||
minRows={1}
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value)
|
||||
//setActionInput(e.target.value)
|
||||
//
|
||||
setContinuationText(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"
|
||||
disabled={continuationText === ""}
|
||||
>
|
||||
<SendIcon
|
||||
color={continuationText === "" ? "disabled" : "primary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
||||
Any failed tasks will be set to ignored (TBD). <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
|
||||
OR
|
||||
</Typography>
|
||||
<Button
|
||||
variant={"contained"}
|
||||
color="primary"
|
||||
disabled={true}
|
||||
style={{marginTop: 10, marginLeft: 20, minWidth: 150, maxWidth: 150, height: 56, }}
|
||||
>
|
||||
Create as Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
: null}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1021,14 +1232,28 @@ const AgentUI = (props) => {
|
||||
|
||||
setAgentActionResult(null)
|
||||
|
||||
document.title = `Agent: ${inputText.substring(0, 30)}...`
|
||||
if (inputText === undefined || inputText === null || inputText === "") {
|
||||
toast.error("Please provide a valid input for the AI Agent.")
|
||||
setAgentRequestLoading(false)
|
||||
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()
|
||||
var parsedAction = "list_tickets,API" // Default action for now
|
||||
if (chosenApps.length > 0) {
|
||||
parsedAction = ""
|
||||
for (var appKey in chosenApps) {
|
||||
const app = chosenApps[appKey]
|
||||
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||
parsedAction += `app:${app.id}:${appname.replaceAll(",", "").replaceAll(":", "")},`
|
||||
}
|
||||
|
||||
parsedAction = parsedAction.slice(0, -1) // Remove last comma
|
||||
}
|
||||
|
||||
const data = {
|
||||
"id": uuid,
|
||||
"name":"agent",
|
||||
@@ -1049,7 +1274,7 @@ const AgentUI = (props) => {
|
||||
},
|
||||
{
|
||||
"name":"action",
|
||||
"value":"list_tickets,API"
|
||||
"value": parsedAction,
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -1080,7 +1305,7 @@ const AgentUI = (props) => {
|
||||
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const handleKeyDownRoot = (e) => {
|
||||
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
||||
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
||||
if (isCmdEnter || isCtrlEnter) {
|
||||
@@ -1089,6 +1314,11 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const chipStyle = {
|
||||
margin: 4,
|
||||
cursor: "pointer",
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={agentWrapperStyle}>
|
||||
<TextField
|
||||
@@ -1100,7 +1330,7 @@ const AgentUI = (props) => {
|
||||
<Box
|
||||
component="form"
|
||||
style={{textAlign: "center", }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={handleKeyDownRoot}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
@@ -1123,7 +1353,7 @@ const AgentUI = (props) => {
|
||||
disabled={agentRequestLoading}
|
||||
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
||||
multiline
|
||||
minRows={2}
|
||||
minRows={1}
|
||||
defaultValue={actionInput || ""}
|
||||
onChange={(e) => {
|
||||
setActionInput(e.target.value)
|
||||
@@ -1143,6 +1373,54 @@ const AgentUI = (props) => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
|
||||
<div>
|
||||
<Chip
|
||||
id="add_app_chip"
|
||||
icon={<AddIcon />} label="Select Apps"
|
||||
style={chipStyle}
|
||||
onClick={() => {
|
||||
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
||||
}}
|
||||
/>
|
||||
<Popover
|
||||
open={appPickerAnchor !== null}
|
||||
anchorEl={appPickerAnchor}
|
||||
onClose={() => {
|
||||
setAppPickerAnchor(null)
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<AppSearch
|
||||
userdata={userdata}
|
||||
defaultSearch={""}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
inputHeight={200}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{chosenApps.map((app, index) => {
|
||||
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
|
||||
const chosenImagePath = app?.image
|
||||
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
|
||||
|
||||
return (
|
||||
<Chip icon={chosenImage} label={chosenName} variant="outlined"
|
||||
style={chipStyle}
|
||||
onDelete={() => {
|
||||
const newChosenApps = chosenApps.filter((a, i) => i !== index)
|
||||
setChosenApps(newChosenApps)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
:
|
||||
<div>
|
||||
|
||||
@@ -135,12 +135,10 @@ import {
|
||||
OpenInFull as OpenInFullIcon,
|
||||
Difference as DifferenceIcon,
|
||||
DataObject as DataObjectIcon,
|
||||
SwapHoriz as SwapHorizIcon
|
||||
} from "@mui/icons-material";
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
//import * as cytoscape from "cytoscape";
|
||||
|
||||
import cytoscape from "cytoscape";
|
||||
|
||||
|
||||
import edgehandles from "cytoscape-edgehandles";
|
||||
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
@@ -152,7 +150,7 @@ import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import LineChartWrapper from "../components/LineChartWrapper.jsx";
|
||||
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows.jsx";
|
||||
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows2.jsx";
|
||||
import { GetParsedPaths, internalIds, } from "../views/Apps.jsx";
|
||||
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
@@ -1065,7 +1063,29 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"type": "",
|
||||
},
|
||||
"description": "Build & integrate tools easily with standard input and standard output. Built by Shuffle. https://singul.io",
|
||||
"actions": [{
|
||||
"actions": [
|
||||
{
|
||||
"name": "Translate standard",
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
"label": "Translate standard",
|
||||
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||
"parameters": [{
|
||||
"name": "source_data",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": true,
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"value": "OCSF",
|
||||
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
|
||||
"options": [
|
||||
"OCSF"
|
||||
],
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
}]
|
||||
}, {
|
||||
"name": "Cases",
|
||||
"description": "Available actions for case management",
|
||||
"label": "Cases",
|
||||
@@ -1277,29 +1297,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
]
|
||||
},
|
||||
*/
|
||||
{
|
||||
"name": "Translate standard",
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
"label": "Translate standard",
|
||||
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||
"parameters": [{
|
||||
"name": "source_data",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": true,
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"value": "OCSF",
|
||||
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
|
||||
"options": [
|
||||
"OCSF"
|
||||
],
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
}]
|
||||
},
|
||||
*/
|
||||
]}]
|
||||
|
||||
|
||||
@@ -2244,6 +2242,11 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
const getWorkflowExecution = (id, execution_id, filter, orgId) => {
|
||||
if (id === undefined) {
|
||||
console.log("No workflow ID defined for getting executions")
|
||||
return
|
||||
}
|
||||
|
||||
var url = `${globalUrl}/api/v2/workflows/${id}/executions`
|
||||
var method = "GET"
|
||||
if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") {
|
||||
@@ -22082,6 +22085,153 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Should probably put this on the backend instead when notifications are made :))
|
||||
const getErrorSuggestion = (result) => {
|
||||
if (result === undefined || result === null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if array with json inside to handle one item at a time~
|
||||
if (typeof result === "object" && result.length !== undefined) {
|
||||
if (result.length > 0) {
|
||||
// Check type inside
|
||||
if (typeof result[0] === "object") {
|
||||
result = result[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success === true && result.status === 200) {
|
||||
if (result.body !== undefined && result.body !== null) {
|
||||
const stringbody = result.body.toString()
|
||||
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (stringbody.length > 1000) {
|
||||
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status === 429) {
|
||||
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
|
||||
}
|
||||
|
||||
if (result.status === 405) {
|
||||
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
|
||||
}
|
||||
|
||||
if (result.status === 415) {
|
||||
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
|
||||
}
|
||||
|
||||
if (result.status === 401) {
|
||||
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 403) {
|
||||
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 404) {
|
||||
return "The URL, or content of the URL is incorrect. Check it and try again."
|
||||
}
|
||||
|
||||
if (result.status === 400) {
|
||||
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 200 || result.status === 201 || result.status === 204) {
|
||||
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
|
||||
}
|
||||
|
||||
|
||||
// Validate and check for newlines
|
||||
if (result.success !== false) {
|
||||
|
||||
var stringjson = result
|
||||
const valid = validateJson(stringjson, true)
|
||||
if (valid.valid === false) {
|
||||
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
|
||||
// Look for newline
|
||||
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
|
||||
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
|
||||
} else {
|
||||
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return ""
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
stringjson = JSON.stringify(result)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
stringjson = stringjson.toLowerCase()
|
||||
if (stringjson.includes("localhost")) {
|
||||
return "You can't use localhost in apps. Use the external ip or url of the server instead"
|
||||
}
|
||||
|
||||
if (stringjson.includes("manifest unknown")) {
|
||||
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("too many values to unpack")) {
|
||||
return "This is a known error with old apps. Please rebuild the app. Contact support@shuffler.io if it persists after rebuild."
|
||||
}
|
||||
|
||||
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
|
||||
return "Consider whether your Orborus environment can connect to a local IP or not."
|
||||
}
|
||||
|
||||
if (stringjson.includes("kms/")) {
|
||||
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
if (stringjson.includes("string indices must be integers")) {
|
||||
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
|
||||
}
|
||||
|
||||
if (stringjson.includes("invalidurl")) {
|
||||
// IF count of "http" is more than one, 1, it's prolly invalid
|
||||
var additionalinfo = ""
|
||||
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
|
||||
additionalinfo = "You may be using multiple 'http' in the URL. "
|
||||
}
|
||||
|
||||
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
|
||||
}
|
||||
|
||||
if (stringjson.includes("result too large to handle")) {
|
||||
return "Execution loading failed. Reload the execution by closing it and clicking it again"
|
||||
}
|
||||
|
||||
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
|
||||
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("invalid header")) {
|
||||
return "A header or authentication token in the app is invalid. Check the app's configuration"
|
||||
}
|
||||
|
||||
|
||||
if (stringjson.includes("connectionerror")) {
|
||||
if (stringjson.includes("kms")) {
|
||||
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
const ShowCopyingTooltip = () => {
|
||||
const [showCopying, setShowCopying] = React.useState(true)
|
||||
|
||||
@@ -22161,8 +22311,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
{executionModalView === 0 ? (
|
||||
|
||||
{executionModalView === 0 ? (
|
||||
<div style={{ position: "relative", padding: isMobile ? "0px 0px 0px 10px" : "25px 25px 25px 25px", zIndex: 12502, backgroundColor: theme.palette.drawer.backgroundColor, height: "100%", }}>
|
||||
<div style={{ display: "flex", }}>
|
||||
<Breadcrumbs
|
||||
@@ -23073,7 +23223,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
height: imgsize,
|
||||
border: `2px solid ${statusColor}`,
|
||||
borderRadius: executionData.start === data.action.id ? 25 : 5,
|
||||
|
||||
cursor: isCloud ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
window.open(`/apps/${data?.action?.app_name}`, "_blank")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23095,7 +23252,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
|
||||
if (data?.action?.name === "User Input" || data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
|
||||
actionimg = (
|
||||
<img
|
||||
alt={"User Input Trigger"}
|
||||
@@ -23222,6 +23379,13 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (relevant_errors.length === 0) {
|
||||
const foundError = getErrorSuggestion(validate.result)
|
||||
if (foundError !== undefined && foundError !== null && foundError !== "") {
|
||||
relevant_errors = [foundError]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -23669,150 +23833,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
)
|
||||
}
|
||||
|
||||
var draggingDisabled = false;
|
||||
|
||||
// Should probably put this on the backend instead when notifications are made :))
|
||||
const getErrorSuggestion = (result) => {
|
||||
if (result === undefined || result === null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if array with json inside to handle one item at a time~
|
||||
if (typeof result === "object" && result.length !== undefined) {
|
||||
if (result.length > 0) {
|
||||
// Check type inside
|
||||
if (typeof result[0] === "object") {
|
||||
result = result[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success === true && result.status === 200) {
|
||||
if (result.body !== undefined && result.body !== null) {
|
||||
const stringbody = result.body.toString()
|
||||
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (stringbody.length > 1000) {
|
||||
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status === 429) {
|
||||
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
|
||||
}
|
||||
|
||||
if (result.status === 405) {
|
||||
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
|
||||
}
|
||||
|
||||
if (result.status === 415) {
|
||||
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
|
||||
}
|
||||
|
||||
if (result.status === 401) {
|
||||
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 403) {
|
||||
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 404) {
|
||||
return "The URL, or content of the URL is incorrect. Check it and try again."
|
||||
}
|
||||
|
||||
if (result.status === 400) {
|
||||
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 200 || result.status === 201 || result.status === 204) {
|
||||
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
|
||||
}
|
||||
|
||||
|
||||
// Validate and check for newlines
|
||||
if (result.success !== false) {
|
||||
|
||||
var stringjson = result
|
||||
const valid = validateJson(stringjson, true)
|
||||
if (valid.valid === false) {
|
||||
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
|
||||
// Look for newline
|
||||
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
|
||||
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
|
||||
} else {
|
||||
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return ""
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
stringjson = JSON.stringify(result)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
stringjson = stringjson.toLowerCase()
|
||||
if (stringjson.includes("localhost")) {
|
||||
return "You can't use localhost in apps. Use the external ip or url of the server instead"
|
||||
}
|
||||
|
||||
if (stringjson.includes("manifest unknown")) {
|
||||
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
|
||||
}
|
||||
|
||||
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
|
||||
return "Consider whether your Orborus environment can connect to a local IP or not."
|
||||
}
|
||||
|
||||
if (stringjson.includes("kms/")) {
|
||||
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
if (stringjson.includes("string indices must be integers")) {
|
||||
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
|
||||
}
|
||||
|
||||
if (stringjson.includes("invalidurl")) {
|
||||
// IF count of "http" is more than one, 1, it's prolly invalid
|
||||
var additionalinfo = ""
|
||||
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
|
||||
additionalinfo = "You may be using multiple 'http' in the URL. "
|
||||
}
|
||||
|
||||
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
|
||||
}
|
||||
|
||||
if (stringjson.includes("result too large to handle")) {
|
||||
return "Execution loading failed. Reload the execution by closing it and clicking it again"
|
||||
}
|
||||
|
||||
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
|
||||
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("invalid header")) {
|
||||
return "A header or authentication token in the app is invalid. Check the app's configuration"
|
||||
}
|
||||
|
||||
|
||||
if (stringjson.includes("connectionerror")) {
|
||||
if (stringjson.includes("kms")) {
|
||||
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
var draggingDisabled = false;
|
||||
|
||||
const currentSuggestion = getErrorSuggestion(validate.result)
|
||||
const codePopoutModal = !codeModalOpen ? null : (
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
|
||||
import { validateJson, } from "../views/Workflows.jsx";
|
||||
import { validateJson, } from "../views/Workflows2.jsx";
|
||||
import { isMobile } from "react-device-detect"
|
||||
import theme from "../theme.jsx";
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
@@ -277,7 +277,8 @@ const ApiExplorerWrapper = (props) => {
|
||||
parsedapp.name !== undefined &&
|
||||
parsedapp.name !== null &&
|
||||
parsedapp.name.length !== 0;
|
||||
if(parsedapp?.id.length > 0){
|
||||
|
||||
if (parsedapp?.id.length > 0) {
|
||||
setSelectedAppData(parsedapp)
|
||||
handleAppAuthenticationType(parsedapp)
|
||||
const apptype = selectedAppData?.generated === false ? "python" : "openapi"
|
||||
@@ -441,7 +442,6 @@ const ApiExplorerWrapper = (props) => {
|
||||
selectedAppData.name = appname
|
||||
}
|
||||
|
||||
console.log("APPNAME: ", appname, openapi.id)
|
||||
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
|
||||
setAppAuthentication(data)
|
||||
setSelectedAuthentication({})
|
||||
|
||||
@@ -3455,9 +3455,9 @@ const AppCreator = (defaultprops) => {
|
||||
required_bodyfields: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Queries: ", urlPathQueries)
|
||||
}, [urlPathQueries])
|
||||
//useEffect(() => {
|
||||
// console.log("Queries: ", urlPathQueries)
|
||||
//}, [urlPathQueries])
|
||||
|
||||
const findBodyParams = (body) => {
|
||||
const regex = /\${(\w+)}/g;
|
||||
|
||||
@@ -74,7 +74,7 @@ import {
|
||||
} from "react-instantsearch-dom";
|
||||
import AppStats from "../components/AppStats.jsx";
|
||||
import ParsedAction from "../components/ParsedAction.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import { base64_decode, appCategories } from "../views/AppCreator.jsx";
|
||||
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
@@ -4454,7 +4454,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
): 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>
|
||||
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}}>Distribute</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+137
-35
@@ -6,7 +6,7 @@ import ReactJson from "react-json-view-ssr";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link, useLocation } from "react-router-dom";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import {
|
||||
@@ -87,6 +87,34 @@ const innerHrefStyle = {
|
||||
textDecoration: "none",
|
||||
};
|
||||
|
||||
const noteLabelStyle = {
|
||||
fontWeight: "bold",
|
||||
color: "#f86a3e",
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
};
|
||||
|
||||
const alertNote = {
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid #f86a3e",
|
||||
backgroundColor: "rgb(26,26,26)",
|
||||
};
|
||||
|
||||
export const Blockquote = ({ children }) => {
|
||||
|
||||
const textContent = children.map(child =>
|
||||
child.props && child.props.children ? child.props.children.join('') : child
|
||||
).join('').trim();
|
||||
|
||||
// Maybe some more contents....
|
||||
const isNote = textContent.startsWith("[!TIP]");
|
||||
return (
|
||||
<blockquote style={isNote ? alertNote : {}}>
|
||||
{isNote && <span style={noteLabelStyle}>Tips:</span>}
|
||||
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
export const CopyToClipboard = (props) => {
|
||||
const { text, style, onCopy } = props;
|
||||
@@ -173,6 +201,86 @@ export const OuterLink = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Markdown table renderers for improved styling and readability
|
||||
export const TableRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflowX: "auto",
|
||||
marginTop: 16,
|
||||
marginBottom: 24,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "separate",
|
||||
borderSpacing: 0,
|
||||
minWidth: 600,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableRowRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
borderBottom: "1px solid rgba(255,255,255,0.12)",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableHeaderCellRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<th
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "12px 14px",
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
color: theme.palette.textColor,
|
||||
fontWeight: 600,
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
borderBottom: "1px solid rgba(255,255,255,0.2)",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableCellRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
verticalAlign: "top",
|
||||
color: theme.palette.textColor,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Img = (props) => {
|
||||
@@ -250,9 +358,23 @@ export const CodeHandler = (props) => {
|
||||
|
||||
// Need to check if it's singletick or multi
|
||||
if (props.inline === true) {
|
||||
// Show it inline
|
||||
// Enhanced inline code styling for readability and long strings
|
||||
return (
|
||||
<span style={{ backgroundColor: theme.palette.inputColor, display: "inline", whiteSpace: "pre-wrap", padding: "6px 3px 6px 3px", }}>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: "6px",
|
||||
padding: "1px 6px",
|
||||
margin: "0 2px",
|
||||
display: "inline-block",
|
||||
lineHeight: 1.6,
|
||||
fontSize: "0.95em",
|
||||
fontFamily:
|
||||
'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{newprop}
|
||||
</span>
|
||||
)
|
||||
@@ -318,6 +440,8 @@ const Docs = (defaultprops) => {
|
||||
props.match = {}
|
||||
props.match.params = params
|
||||
|
||||
window.title = "Shuffle - Documentation"
|
||||
|
||||
//console.log("PARAMS: ", params)
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
@@ -668,36 +792,13 @@ const Docs = (defaultprops) => {
|
||||
minHeight: "80vh",
|
||||
};
|
||||
|
||||
const noteLabelStyle = {
|
||||
fontWeight: "bold",
|
||||
color: "#f86a3e",
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
};
|
||||
|
||||
const Blockquote = ({ children }) => {
|
||||
|
||||
const textContent = children.map(child =>
|
||||
child.props && child.props.children ? child.props.children.join('') : child
|
||||
).join('').trim();
|
||||
|
||||
// Maybe some more contents....
|
||||
const isNote = textContent.startsWith("[!TIP]");
|
||||
return (
|
||||
<blockquote style={isNote ? alertNote : {}}>
|
||||
{isNote && <span style={noteLabelStyle}>Tips:</span>}
|
||||
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Heading = (props) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||
if (props.level <= 3) {
|
||||
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
||||
var id = (props?.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||
if (props?.level <= 3) {
|
||||
id = props?.children?.[0]?.toLowerCase().toString().replaceAll(" ", "-") ?? '';
|
||||
}
|
||||
|
||||
const element = React.createElement(
|
||||
@@ -1098,11 +1199,6 @@ const Docs = (defaultprops) => {
|
||||
fontSize: isMobile ? "1.3rem" : "1.1rem",
|
||||
};
|
||||
|
||||
const alertNote = {
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid #f86a3e",
|
||||
backgroundColor: "rgb(26,26,26)",
|
||||
};
|
||||
|
||||
const CustomButton = (props) => {
|
||||
const { title, icon, link } = props
|
||||
@@ -1192,7 +1288,7 @@ const Docs = (defaultprops) => {
|
||||
</Typography>
|
||||
{showPartnerLogo === true ? null :
|
||||
<div style={{ display: "flex", marginTop: 25, }}>
|
||||
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> />
|
||||
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/contact?category=support" />
|
||||
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
|
||||
</div>
|
||||
}
|
||||
@@ -1230,6 +1326,8 @@ const Docs = (defaultprops) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
const markdownComponents = {
|
||||
img: Img,
|
||||
code: CodeHandler,
|
||||
@@ -1242,6 +1340,10 @@ const Docs = (defaultprops) => {
|
||||
a: OuterLink,
|
||||
p: Paragraph,
|
||||
blockquote: Blockquote,
|
||||
table: TableRenderer,
|
||||
tr: TableRowRenderer,
|
||||
th: TableHeaderCellRenderer,
|
||||
td: TableCellRenderer,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ const LoginPage = props => {
|
||||
const tmpMessage = new URLSearchParams(window.location.search).get("message")
|
||||
if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) {
|
||||
setMessage(tmpMessage)
|
||||
toast(tmpMessage)
|
||||
toast.warn(tmpMessage)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
Select,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
@@ -54,6 +56,15 @@ const NewDashboard = (props) => {
|
||||
const [overrideDays, setOverrideDays] = useState(undefined);
|
||||
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
|
||||
const [isProdStatusOn, setIsProdStatusOn] = useState(false)
|
||||
const [selectedOrganization, setSelectedOrganization] = useState(null)
|
||||
const [selectedOrgForStats, setSelectedOrgForStats] = useState(userdata?.active_org?.id || null)
|
||||
const [availableOrgs, setAvailableOrgs] = useState([])
|
||||
const [selectedOrgStats, setSelectedOrgStats] = useState(null)
|
||||
const [loadingSelectedOrgStats, setLoadingSelectedOrgStats] = useState(false)
|
||||
const [selectedOrgDetailsForStats, setSelectedOrgDetailsForStats] = useState(null)
|
||||
const [fallbackToParentStats, setFallbackToParentStats] = useState(false);
|
||||
|
||||
document.title = "Shuffle - Dashboard";
|
||||
|
||||
const isCloud =
|
||||
serverside === true || typeof window === "undefined"
|
||||
@@ -121,15 +132,22 @@ const NewDashboard = (props) => {
|
||||
const displayName = userdata !== undefined && userdata?.username !== undefined ? userdata?.username?.split('@')[0]?.charAt(0)?.toUpperCase() + userdata?.username?.split('@')[0]?.slice(1) : 'User';
|
||||
|
||||
useEffect(() => {
|
||||
let t;
|
||||
const anyLoading = loadingSfw || loadingRot || loadingNoti;
|
||||
if (anyLoading) {
|
||||
t = setShowOverlay(true);
|
||||
} else {
|
||||
setShowOverlay(false);
|
||||
}
|
||||
return () => { if (t) clearTimeout(t); };
|
||||
}, [loadingSfw, loadingRot, loadingNoti]);
|
||||
const anyLoading =
|
||||
loadingSfw ||
|
||||
loadingRot ||
|
||||
loadingNoti ||
|
||||
loadingSelectedOrgStats ||
|
||||
!selectedOrganization ||
|
||||
!selectedOrgForStats;
|
||||
setShowOverlay(anyLoading);
|
||||
}, [
|
||||
loadingSfw,
|
||||
loadingRot,
|
||||
loadingNoti,
|
||||
loadingSelectedOrgStats,
|
||||
selectedOrganization,
|
||||
selectedOrgForStats,
|
||||
]);
|
||||
|
||||
// Auto-open onboarding when there aren't enough active days of stats
|
||||
useEffect(() => {
|
||||
@@ -211,6 +229,7 @@ const NewDashboard = (props) => {
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((org) => {
|
||||
if (!fetched && org) {
|
||||
setSelectedOrganization(org);
|
||||
if (!isCloud) {
|
||||
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||
setIsProdStatusOn(true);
|
||||
@@ -229,8 +248,136 @@ const NewDashboard = (props) => {
|
||||
};
|
||||
}, [userdata?.active_org?.id, globalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOrgForStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fetched = false;
|
||||
fetch(`${globalUrl}/api/v1/orgs/${selectedOrgForStats}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((org) => {
|
||||
if (!fetched && org) {
|
||||
setSelectedOrgDetailsForStats(org);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => {
|
||||
fetched = true;
|
||||
};
|
||||
}, [selectedOrgForStats, globalUrl]);
|
||||
|
||||
|
||||
// Build list of available orgs for stats (parent + child orgs)
|
||||
useEffect(() => {
|
||||
if (!selectedOrganization) return;
|
||||
|
||||
const orgs = [{ id: 'ALL', name: 'All Organizations', isAll: true }];
|
||||
|
||||
// Add parent org
|
||||
if (selectedOrganization?.id) {
|
||||
orgs.push({
|
||||
id: selectedOrganization.id,
|
||||
name: selectedOrganization.name || 'Parent Organization',
|
||||
isAll: false
|
||||
});
|
||||
}
|
||||
|
||||
// Add child orgs if they exist
|
||||
if (selectedOrganization?.child_orgs && Array.isArray(selectedOrganization.child_orgs)) {
|
||||
selectedOrganization.child_orgs.forEach(child => {
|
||||
if (child?.id && child?.name) {
|
||||
orgs.push({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
isAll: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAvailableOrgs(orgs);
|
||||
setSelectedOrgForStats(selectedOrganization?.id);
|
||||
}, [selectedOrganization]);
|
||||
|
||||
// Fetch statistics for specifically selected org (not for ALL)
|
||||
useEffect(() => {
|
||||
|
||||
let aborted = false;
|
||||
const load = async () => {
|
||||
try {
|
||||
if (!selectedOrgForStats || selectedOrgForStats === 'ALL') {
|
||||
setSelectedOrgStats(null);
|
||||
setFallbackToParentStats(false);
|
||||
return;
|
||||
}
|
||||
setLoadingSelectedOrgStats(true);
|
||||
const resp = await fetch(`${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
let fallback = false;
|
||||
let data = null;
|
||||
if (resp.ok) {
|
||||
data = await resp.json();
|
||||
}
|
||||
const isParentContext = selectedOrganization && userdata?.active_org?.id === selectedOrganization.id;
|
||||
if (!data || !Array.isArray(data.daily_statistics) || data.daily_statistics.length === 0) {
|
||||
fallback = true;
|
||||
setSelectedOrgStats(null);
|
||||
// Only fallback to parent if on parent org dashboard
|
||||
if (isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
|
||||
toast.info('No stats available for selected org. Showing parent org stats.');
|
||||
setSelectedOrgForStats(selectedOrganization.id);
|
||||
}
|
||||
} else {
|
||||
// If data exists but has near-zero activity over the last 30 days, fallback to parent
|
||||
try {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 30);
|
||||
const recent = data.daily_statistics.filter((d) => {
|
||||
if (!d?.date) return false;
|
||||
const dt = new Date(d.date);
|
||||
return dt >= cutoff;
|
||||
});
|
||||
const sumWork = recent.reduce((s, d) => s + Number(d?.workflow_executions_finished || 0) + Number(d?.workflow_executions_failed || 0), 0);
|
||||
const sumApp = recent.reduce((s, d) => s + Number(d?.app_executions || 0), 0);
|
||||
const nearZero = (Number(sumWork) + Number(sumApp)) <= 0;
|
||||
if (nearZero && isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
|
||||
fallback = true;
|
||||
setSelectedOrgStats(null);
|
||||
toast.info('Selected org has no activity in the last 30 days. Showing parent org stats.');
|
||||
setSelectedOrgForStats(selectedOrganization.id);
|
||||
} else {
|
||||
setSelectedOrgStats(data || null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedOrgStats(data || null);
|
||||
}
|
||||
}
|
||||
setFallbackToParentStats(fallback && isParentContext);
|
||||
} catch {
|
||||
if (!aborted) setSelectedOrgStats(null);
|
||||
setFallbackToParentStats(false);
|
||||
} finally {
|
||||
if (!aborted) setLoadingSelectedOrgStats(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calling the function.
|
||||
load();
|
||||
return () => { aborted = true; };
|
||||
}, [selectedOrgForStats, selectedOrganization, globalUrl, userdata?.active_org?.id]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
|
||||
{(onboardingOpen && !loadingSelectedOrgStats) && (
|
||||
<DashboardOnboarding
|
||||
open={onboardingOpen}
|
||||
globalUrl={globalUrl}
|
||||
@@ -251,6 +398,7 @@ const NewDashboard = (props) => {
|
||||
headerTitle="Unlock your Dashboard"
|
||||
headerSubtitle="Complete these steps to start seeing insights."
|
||||
/>
|
||||
)}
|
||||
{showOverlay && (
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
@@ -262,9 +410,33 @@ const NewDashboard = (props) => {
|
||||
{/* Header / Greeting */}
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
||||
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
||||
<>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{availableOrgs.length > 2 && (
|
||||
<FormControl size="small" variant="outlined" style={{ minWidth: 200, marginTop: -8 }} sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
height: 40,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
borderRadius: '20px',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
|
||||
}}>
|
||||
<InputLabel id="stats-org-select-label">View Stats For</InputLabel>
|
||||
<Select
|
||||
labelId="stats-org-select-label"
|
||||
label="View Stats"
|
||||
value={selectedOrgForStats || ''}
|
||||
onChange={(e) => setSelectedOrgForStats(e.target.value)}
|
||||
>
|
||||
{availableOrgs.map((org) => (
|
||||
<MenuItem key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{sfwControls}
|
||||
</>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* KPI cards */}
|
||||
@@ -309,12 +481,27 @@ const NewDashboard = (props) => {
|
||||
onControlsChange={handleSfwControls}
|
||||
onLoadingChange={setLoadingSfw}
|
||||
onTotalsChange={setTotals}
|
||||
loadingSelectedOrgStats={loadingSelectedOrgStats}
|
||||
selectedOrganization={selectedOrganization}
|
||||
selectedOrgForStats={selectedOrgForStats}
|
||||
orgStats={selectedOrgStats}
|
||||
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Runs over time section */}
|
||||
<Paper style={{ padding: 16, marginTop: 19, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12 }}>
|
||||
<RunsOverTimeWidget globalUrl={globalUrl} onLoadingChange={setLoadingRot} monthOverride={rotMonthOverride} dummyMode={onboardingOpen} />
|
||||
<RunsOverTimeWidget
|
||||
globalUrl={globalUrl}
|
||||
onLoadingChange={setLoadingRot}
|
||||
loadingSelectedOrgStats={loadingSelectedOrgStats}
|
||||
monthOverride={rotMonthOverride}
|
||||
dummyMode={onboardingOpen}
|
||||
selectedOrganization={selectedOrganization}
|
||||
selectedOrgForStats={selectedOrgForStats}
|
||||
orgStats={selectedOrgStats}
|
||||
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
|
||||
/>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,10 +3,10 @@ import React, {useState, useEffect, useContext} from 'react';
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { green, yellow, red, grey} from "./AngularWorkflow.jsx";
|
||||
import { green, yellow, red, grey} from "../views/AngularWorkflow.jsx";
|
||||
import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import { validateJson, collapseField, GetIconInfo } from "./Workflows.jsx";
|
||||
import { validateJson, collapseField, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import EditWorkflow from "../components/EditWorkflow.jsx"
|
||||
import { toast } from "react-toastify"
|
||||
import { makeStyles } from '@mui/material/styles';
|
||||
@@ -47,6 +47,10 @@ import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Edit as EditIcon,
|
||||
Polyline as PolylineIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
DirectionsRun as DirectionsRunIcon,
|
||||
Error as ErrorIcon,
|
||||
Pause as PauseIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
@@ -145,6 +149,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
padding: "25px 50px 50px 50px",
|
||||
borderRadius: 25,
|
||||
minHeight: 500,
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const params = useParams();
|
||||
@@ -189,7 +194,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
// questions if none are selected
|
||||
for (var key in executionArgument) {
|
||||
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
|
||||
console.log("Unanswered, required question: ", key)
|
||||
//console.log("Unanswered, required question: ", key)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -406,7 +411,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
setTimeout(() => {
|
||||
setExecutionLoading(true)
|
||||
}, 2500)
|
||||
}, 250)
|
||||
|
||||
var data = {
|
||||
"execution_argument": executionArgument,
|
||||
@@ -842,6 +847,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -915,20 +921,37 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
//console.log("Updating data!")
|
||||
setExecutionData(responseJson)
|
||||
|
||||
for (var key in responseJson.results) {
|
||||
if (responseJson.results[key].status === "WAITING") {
|
||||
const validate = validateJson(responseJson.results[key].result)
|
||||
if (validate.valid && typeof validate.result === "string") {
|
||||
validate.result = JSON.parse(validate.result)
|
||||
}
|
||||
|
||||
if (validate.result["information"] !== undefined && validate.result["information"] !== null) {
|
||||
setWorkflowQuestion(validate.result["information"])
|
||||
}
|
||||
|
||||
break
|
||||
if (responseJson.results[key].status !== "WAITING") {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
const validate = validateJson(responseJson.results[key].result)
|
||||
if (validate.valid && typeof validate.result === "string") {
|
||||
validate.result = JSON.parse(validate.result)
|
||||
}
|
||||
|
||||
console.log("Found waiting!: ", validate.result)
|
||||
|
||||
if (validate?.result?.information !== undefined && validate?.result?.information !== null) {
|
||||
console.log("Success! Not checking again.")
|
||||
setWorkflowQuestion(validate?.result?.information)
|
||||
} else {
|
||||
console.log("No information found for questions?: ", validate.result)
|
||||
|
||||
// Specific case for "too early" config of waiting
|
||||
if (typeof validate.result === "string" || Object.keys(validate.result).length === 2) {
|
||||
setTimeout(() => {
|
||||
fetchUpdates(responseJson.execution_id, responseJson.authorization)
|
||||
}, 2000)
|
||||
} else {
|
||||
console.log("NOT re-fetching")
|
||||
}
|
||||
//setWorkflowQuestions{
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
} else {
|
||||
console.log("NOT updating executiondata state.");
|
||||
@@ -1034,7 +1057,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for stream results :O!");
|
||||
|
||||
toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
|
||||
//toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -1045,18 +1068,19 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && disableButtons === false && responseJson?.status !== "" && responseJson?.status !== "WAITING") {
|
||||
console.log("IN here 1")
|
||||
setDisableButtons(true)
|
||||
}
|
||||
|
||||
//if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) {
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && responseJson.workflow !== undefined && responseJson.workflow !== null) {
|
||||
console.log("IN here 2")
|
||||
setupSourcenode(responseJson.workflow, sourceNode)
|
||||
setWorkflow(responseJson.workflow)
|
||||
|
||||
//const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||
// Check for decision_id in url
|
||||
if (decisionId?.length > 0 && responseJson?.workflow?.actions?.length > 0 && sourceNode?.length > 0 && responseJson?.results?.length > 0) {
|
||||
console.log("Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
|
||||
console.log("AGENTIC: Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
|
||||
|
||||
setAgentic(true)
|
||||
|
||||
@@ -1409,6 +1433,27 @@ const RunWorkflow = (defaultprops) => {
|
||||
const basedata =
|
||||
<div style={bodyDivStyle}>
|
||||
<Paper style={boxStyle}>
|
||||
<div style={{position: "absolute", top: 20, right: 10,}}>
|
||||
{executionData?.status === "" ? null :
|
||||
executionData?.status === "FINISHED" ?
|
||||
<Tooltip title="The previous Workflow Finished" placement="top">
|
||||
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "EXECUTING" ?
|
||||
<Tooltip title="The Workflow is current running" placement="top">
|
||||
<DirectionsRunIcon style={{color: theme.palette.secondary, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "ABORTED" || executionData?.status === "FAILURE" ?
|
||||
<Tooltip title={`The workflow run failed with status ${executionData?.status}`} placement="top">
|
||||
<ErrorIcon style={{color: red, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "WAITING" ?
|
||||
<Tooltip title={`The workflow is waiting for user input`} placement="top">
|
||||
<PauseIcon style={{color: yellow, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{explorerUi === true ?
|
||||
<ExplorerUi />
|
||||
:
|
||||
@@ -1562,7 +1607,6 @@ const RunWorkflow = (defaultprops) => {
|
||||
label={parsedLabel}
|
||||
required
|
||||
|
||||
disabled={disabledButtons}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
@@ -1582,43 +1626,46 @@ const RunWorkflow = (defaultprops) => {
|
||||
:
|
||||
(answer !== undefined && answer !== null) || message !== "" ? null :
|
||||
|
||||
<span>
|
||||
{foundSourcenode !== undefined && foundSourcenode !== null ?
|
||||
"Add Note"
|
||||
:
|
||||
"Runtime Argument"
|
||||
}
|
||||
executionRunning ? null :
|
||||
<span>
|
||||
{foundSourcenode !== undefined && foundSourcenode !== null ?
|
||||
"Add Note"
|
||||
:
|
||||
"Runtime Argument"
|
||||
}
|
||||
|
||||
<div style={{marginBottom: 5}}>
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
|
||||
multiLine
|
||||
maxRows={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
autocomplete: "off",
|
||||
form: {
|
||||
<div style={{marginBottom: 5}}>
|
||||
<TextField
|
||||
disabled={executionRunning}
|
||||
color="primary"
|
||||
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
|
||||
multiLine
|
||||
maxRows={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
autocomplete: "off",
|
||||
},
|
||||
style:{
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
setExecutionArgument(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
form: {
|
||||
autocomplete: "off",
|
||||
},
|
||||
style:{
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
setExecutionArgument(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1724,12 +1771,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
>
|
||||
{executionLoading ?
|
||||
<CircularProgress color="secondary" style={{color: "white",}} />
|
||||
:
|
||||
executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ? "Run Again"
|
||||
:
|
||||
"Submit"
|
||||
}
|
||||
</Button>
|
||||
: executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ?
|
||||
"Run Again"
|
||||
: executionData?.status === "WAITING" ?
|
||||
"Submit answers"
|
||||
: "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1780,7 +1827,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: 10, }}>
|
||||
<div style={{marginBottom: 10, borderBottom: "1px solid rgba(255,255,255,0.3)", paddingBottom: 5, }} key={index}>
|
||||
{foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}:
|
||||
<br />
|
||||
|
||||
@@ -1807,7 +1854,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
{workflowQuestion !== "" ? null :
|
||||
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 10, }} >
|
||||
Form submission data includes your Organization's unique ID while logged in, or a unique identifier for your browser otherwise. Your input will be automatically sanitized.
|
||||
Form submission data includes your Organization's unique ID, or a unique identifier for your browser. Your input will be automatically sanitized.
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -126,296 +126,6 @@ const useStyles = makeStyles(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Takes an action in Shuffle and
|
||||
// Returns information about the icon, the color etc to be used
|
||||
// This can be used for actions of all types
|
||||
export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cases", values: ["cases"] },
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter"] },
|
||||
{ key: "merge", values: ["join", "merge", "route", "router", "routing"] },
|
||||
{
|
||||
key: "search",
|
||||
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
|
||||
},
|
||||
{ key: "list", values: ["list", "head", "options"] },
|
||||
{
|
||||
key: "download",
|
||||
values: [
|
||||
"capture",
|
||||
"get",
|
||||
"download",
|
||||
"return",
|
||||
"hello_world",
|
||||
"curl",
|
||||
"request",
|
||||
"export",
|
||||
"preview",
|
||||
],
|
||||
},
|
||||
{ key: "add", values: ["add", "accept",] },
|
||||
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
|
||||
{
|
||||
key: "send",
|
||||
values: [
|
||||
"send",
|
||||
"dispatch",
|
||||
"mail",
|
||||
"forward",
|
||||
"post",
|
||||
"submit",
|
||||
"mark",
|
||||
"set",
|
||||
"release",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "repeat",
|
||||
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
|
||||
},
|
||||
{ key: "code", values: ["code", "bash", "python", "react", "go"] },
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise"] },
|
||||
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
|
||||
{ key: "inflate", values: ["inflate", "pack", "compress"] },
|
||||
{
|
||||
key: "edit",
|
||||
values: [
|
||||
"modify",
|
||||
"update",
|
||||
"create",
|
||||
"edit",
|
||||
"put",
|
||||
"patch",
|
||||
"change",
|
||||
"replace",
|
||||
"conver",
|
||||
"map",
|
||||
"format",
|
||||
"escape",
|
||||
"describe",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "compare",
|
||||
values: ["compare", "convert", "to", "filter", "translate", "parse", "generate", ],
|
||||
},
|
||||
{ key: "close", values: ["close", "stop", "cancel", "block"] },
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
];
|
||||
|
||||
var selectedKey = ""
|
||||
if (action.app_name == "Integration Framework") {
|
||||
selectedKey = "magic"
|
||||
} else if (action.name === undefined || action.name === null) {
|
||||
} else {
|
||||
const actionname = action.name.toLowerCase()
|
||||
for (var key in iconList) {
|
||||
//console.log(iconList[key], actionname)
|
||||
const found = iconList[key].values.find((value) =>
|
||||
actionname.includes(value)
|
||||
)
|
||||
if (found !== null && found !== undefined) {
|
||||
selectedKey = iconList[key].key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some of these are manually parsed or created instead of material ui
|
||||
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
|
||||
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
|
||||
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
|
||||
const defaultColor = "#f76b1c";
|
||||
const defaultGradient = ["#fad961", "#f76b1c"];
|
||||
const parsedIcons = {
|
||||
magic: {
|
||||
icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "red",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#8A2BE2"],
|
||||
},
|
||||
communication: {
|
||||
icon: "M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cases: {
|
||||
icon: "M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cache_add: {
|
||||
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cache_get: {
|
||||
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
repeat: {
|
||||
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CachedIcon />,
|
||||
},
|
||||
add: {
|
||||
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <AddIcon />,
|
||||
},
|
||||
edit: {
|
||||
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <EditIcon />,
|
||||
},
|
||||
filter: {
|
||||
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
merge: {
|
||||
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
compare: {
|
||||
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CompareIcon />,
|
||||
},
|
||||
extract: {
|
||||
icon: "M3 3h18v2H3z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MaximizeIcon />,
|
||||
},
|
||||
inflate: {
|
||||
icon: "M6 19h12v2H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MinimizeIcon />,
|
||||
},
|
||||
list: {
|
||||
icon: "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <TocIcon />,
|
||||
},
|
||||
code: {
|
||||
icon: "M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CodeIcon />,
|
||||
fillGradient: ["#836ef5", "#3335eb"],
|
||||
},
|
||||
execute: {
|
||||
icon: "M8 5v14l11-7z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <PlayArrowIcon />,
|
||||
},
|
||||
delete: {
|
||||
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <DeleteIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
close: {
|
||||
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <CloseIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
send: {
|
||||
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <SendIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
download: {
|
||||
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <GetAppIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
search: {
|
||||
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "green",
|
||||
originalIcon: <SearchIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
var selectedItem = parsedIcons[selectedKey];
|
||||
if (selectedItem === undefined || selectedItem === null) {
|
||||
return {
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
iconBackground: "black",
|
||||
originalIcon: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedItem.fillGradient === undefined) {
|
||||
selectedItem.fillGradient = defaultGradient;
|
||||
selectedItem.iconBackgroundColor = defaultColor;
|
||||
}
|
||||
|
||||
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
|
||||
console.log(
|
||||
`MISSING PATH FOR ${selectedKey} (find in scope): `,
|
||||
selectedItem.originalIcon.type.type
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(selectedItem.originalIcon === undefined ||
|
||||
selectedItem.originalIcon === "") &&
|
||||
selectedItem.icon !== "" &&
|
||||
selectedItem.icon !== undefined
|
||||
) {
|
||||
const svg_pin = (
|
||||
<svg
|
||||
width={svgSize}
|
||||
height={svgSize}
|
||||
viewBox={`0 0 ${svgSize} ${svgSize}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
|
||||
</svg>
|
||||
);
|
||||
selectedItem.originalIcon = svg_pin;
|
||||
}
|
||||
|
||||
return selectedItem;
|
||||
};
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
marginRight: 5,
|
||||
@@ -427,292 +137,6 @@ const chipStyle = {
|
||||
color: "white",
|
||||
};
|
||||
|
||||
export const collapseField = (field, inputdata) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.name === "result") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
if (typeof copy.name === "string") {
|
||||
copy.name = copy.name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
//lol
|
||||
if (typeof base === 'object' || typeof base === 'dict') {
|
||||
base = JSON.stringify(base)
|
||||
}
|
||||
|
||||
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
|
||||
base_node_name = "exec"
|
||||
}
|
||||
|
||||
//console.log("COPY: ", base_node_name, copy);
|
||||
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
|
||||
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.namespace[copykey]];
|
||||
if (!isNaN(copy.namespace[copykey])) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.namespace[copykey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.name];
|
||||
if (!isNaN(copy.name)) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.name;
|
||||
}
|
||||
}
|
||||
|
||||
to_be_copied = to_be_copied.replaceAll(" ", "_");
|
||||
console.log("COPY: ", to_be_copied);
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
//console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(to_be_copied);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
//console.log("COPYING!");
|
||||
toast("Copied JSON path to clipboard.")
|
||||
} else {
|
||||
console.log("Couldn't find element ", elementName);
|
||||
}
|
||||
}
|
||||
|
||||
export const handleReactJsonClipboard = (copy) => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
if (
|
||||
copy.namespace !== undefined &&
|
||||
copy.name !== undefined &&
|
||||
copy.src !== undefined
|
||||
) {
|
||||
copy = copy.src;
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
var stringified = JSON.stringify(copy);
|
||||
if (stringified.startsWith('"') && stringified.endsWith('"')) {
|
||||
stringified = stringified.substring(1, stringified.length - 1);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(stringified);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
|
||||
console.log("COPYING!");
|
||||
toast.success("Copied Value, NOT json path.")
|
||||
} else {
|
||||
console.log("Failed to copy from " + elementName + ": ", copyText);
|
||||
}
|
||||
}
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
if (showResult === undefined || showResult === null) {
|
||||
return {
|
||||
valid: false,
|
||||
result: "",
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof showResult === 'string') {
|
||||
showResult = showResult.split(" False").join(" false")
|
||||
showResult = showResult.split(" True").join(" true")
|
||||
|
||||
showResult.replaceAll("False,", "false,")
|
||||
showResult.replaceAll("True,", "true,")
|
||||
}
|
||||
|
||||
if (typeof showResult === "object" || typeof showResult === "array") {
|
||||
return {
|
||||
valid: true,
|
||||
result: showResult,
|
||||
}
|
||||
}
|
||||
|
||||
if (showResult[0] === "\"") {
|
||||
return {
|
||||
valid: false,
|
||||
result: showResult,
|
||||
}
|
||||
}
|
||||
|
||||
var jsonvalid = true
|
||||
try {
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false
|
||||
|
||||
return {
|
||||
valid: jsonvalid,
|
||||
result: showResult,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
try {
|
||||
showResult = showResult.split("'").join('"');
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
jsonvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
var result = showResult;
|
||||
try {
|
||||
result = jsonvalid ? JSON.parse(showResult, {"storeAsString": true}) : showResult;
|
||||
} catch (e) {
|
||||
////console.log("Failed parsing JSON even though its valid: ", e)
|
||||
jsonvalid = false;
|
||||
}
|
||||
|
||||
if (jsonvalid === false) {
|
||||
|
||||
if (typeof showResult === 'string') {
|
||||
showResult = showResult.trim()
|
||||
}
|
||||
|
||||
try {
|
||||
var newstr = showResult.replaceAll("'", '"')
|
||||
|
||||
// Basic workarounds for issues with Python Dicts -> JSON
|
||||
if (newstr.includes(": None")) {
|
||||
newstr = newstr.replaceAll(": None", ': null')
|
||||
}
|
||||
|
||||
if (newstr.includes("[\"{") && newstr.includes("}\"]")) {
|
||||
newstr = newstr.replaceAll("[\"{", '[{')
|
||||
newstr = newstr.replaceAll("}\"]", '}]')
|
||||
}
|
||||
|
||||
if (newstr.includes("{\"[") && newstr.includes("]\"}")) {
|
||||
newstr = newstr.replaceAll("{\"[", '[{')
|
||||
newstr = newstr.replaceAll("]\"}", '}]')
|
||||
}
|
||||
|
||||
result = JSON.parse(newstr)
|
||||
jsonvalid = true
|
||||
} catch (e) {
|
||||
|
||||
//console.log("Failed parsing JSON even though its valid (2): ", e)
|
||||
jsonvalid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonvalid && typeof result === "number") {
|
||||
jsonvalid = false
|
||||
}
|
||||
|
||||
// This is where we start recursing
|
||||
if (jsonvalid) {
|
||||
// Check fields if they can be parsed too
|
||||
try {
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
|
||||
//console.log("CHECKING STRING: ", value)
|
||||
|
||||
const inside_result = validateJson(value)
|
||||
if (inside_result.valid) {
|
||||
//console.log("INSIDE RESULT: ", inside_result.result)
|
||||
|
||||
if (typeof inside_result.result === "string") {
|
||||
const newres = JSON.parse(inside_result.result)
|
||||
|
||||
result[key] = newres
|
||||
} else {
|
||||
result[key] = inside_result.result
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Usually only reaches here if raw array > dict > value
|
||||
if (typeof showResult !== "array") {
|
||||
for (const [subkey, subvalue] of Object.entries(value)) {
|
||||
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
|
||||
const inside_result = validateJson(subvalue)
|
||||
if (inside_result.valid) {
|
||||
if (typeof inside_result.result === "string") {
|
||||
const newres = JSON.parse(inside_result.result)
|
||||
result[key][subkey] = newres
|
||||
} else {
|
||||
result[key][subkey] = inside_result.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//console.log("Failed parsing inside json subvalues: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: jsonvalid,
|
||||
result: result,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
//Custom hook for handling styling of the dropzone
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
List as ListIcon,
|
||||
Publish as PublishIcon,
|
||||
GetApp as GetAppIcon,
|
||||
Image as ImageIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
// Additional Components
|
||||
@@ -139,8 +140,115 @@ const getCookie = (name) => {
|
||||
return "";
|
||||
};
|
||||
|
||||
export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
if (typeof copy.name === "string") {
|
||||
copy.name = copy.name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
//lol
|
||||
if (typeof base === 'object' || typeof base === 'dict') {
|
||||
base = JSON.stringify(base)
|
||||
}
|
||||
|
||||
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
|
||||
base_node_name = "exec"
|
||||
}
|
||||
|
||||
//console.log("COPY: ", base_node_name, copy);
|
||||
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
|
||||
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.namespace[copykey]];
|
||||
if (!isNaN(copy.namespace[copykey])) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.namespace[copykey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.name];
|
||||
if (!isNaN(copy.name)) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.name;
|
||||
}
|
||||
}
|
||||
|
||||
to_be_copied = to_be_copied.replaceAll(" ", "_");
|
||||
console.log("COPY: ", to_be_copied);
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
//console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(to_be_copied).catch(() => {
|
||||
const temp = document.createElement("textarea");
|
||||
temp.value = to_be_copied;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(temp);
|
||||
});
|
||||
//console.log("COPYING!");
|
||||
toast("Copied JSON path to clipboard.")
|
||||
} else {
|
||||
console.log("Couldn't find element ", elementName);
|
||||
}
|
||||
}
|
||||
|
||||
export const handleReactJsonClipboard = (copy) => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
if (
|
||||
copy.namespace !== undefined &&
|
||||
copy.name !== undefined &&
|
||||
copy.src !== undefined
|
||||
) {
|
||||
copy = copy.src;
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
var stringified = JSON.stringify(copy);
|
||||
if (stringified.startsWith('"') && stringified.endsWith('"')) {
|
||||
stringified = stringified.substring(1, stringified.length - 1);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(stringified).catch(() => {
|
||||
const temp = document.createElement("textarea");
|
||||
temp.value = stringified;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(temp);
|
||||
});
|
||||
|
||||
console.log("COPYING!");
|
||||
toast.success("Copied Value, NOT json path.")
|
||||
} else {
|
||||
console.log("Failed to copy from " + elementName + ": ", copyText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -150,7 +258,7 @@ const getCookie = (name) => {
|
||||
export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cases", values: ["cases", "ticket", "alert"] },
|
||||
{ key: "cases", values: ["cases", "ticket", "tickets", "alert"] },
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter"] },
|
||||
@@ -181,7 +289,7 @@ export const GetIconInfo = (action) => {
|
||||
key: "repeat",
|
||||
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
|
||||
},
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise"] },
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise", "control", "ctrl"] },
|
||||
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
|
||||
{ key: "inflate", values: ["inflate", "pack", "compress"] },
|
||||
{
|
||||
@@ -211,7 +319,6 @@ export const GetIconInfo = (action) => {
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
{ key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] },
|
||||
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
|
||||
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] },
|
||||
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
|
||||
{
|
||||
key: "send",
|
||||
@@ -235,7 +342,8 @@ export const GetIconInfo = (action) => {
|
||||
"passwd",
|
||||
"protect",
|
||||
],
|
||||
}
|
||||
},
|
||||
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", ] },
|
||||
];
|
||||
|
||||
var selectedKey = ""
|
||||
@@ -482,29 +590,35 @@ export const GetIconInfo = (action) => {
|
||||
return selectedItem;
|
||||
};
|
||||
|
||||
export const collapseField = (field, inputdata) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
export const collapseField = (field) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.name === "result") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
@@ -700,7 +814,8 @@ const Workflows2 = (props) => {
|
||||
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 [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true")
|
||||
const [showWorkflowImages, setShowWorkflowImages] = React.useState(localStorage?.getItem("showWorkflowImages") === "true")
|
||||
|
||||
const imgSize = 60;
|
||||
|
||||
@@ -2158,7 +2273,7 @@ const Workflows2 = (props) => {
|
||||
position: "relative",
|
||||
borderRadius: "8px",
|
||||
// backgroundColor: "#212121",
|
||||
padding: "15px 15px 0px 20px",
|
||||
padding: "15px 15px 0px 30px",
|
||||
};
|
||||
|
||||
const gridContainer = {
|
||||
@@ -2177,7 +2292,6 @@ const Workflows2 = (props) => {
|
||||
justifyContent: "space-between",
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
textAlign: "center",
|
||||
margin: "auto",
|
||||
};
|
||||
|
||||
const exportAllWorkflows = (allWorkflows) => {
|
||||
@@ -2959,7 +3073,7 @@ const Workflows2 = (props) => {
|
||||
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
|
||||
if (foundOrg !== undefined && foundOrg !== null) {
|
||||
//position: "absolute", bottom: 5, right: -5,
|
||||
imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
|
||||
imageStyle.border = foundOrg.id === userdata.active_org.id ? `2px solid ${boxColor}` : null
|
||||
|
||||
|
||||
image =
|
||||
@@ -2997,10 +3111,10 @@ const Workflows2 = (props) => {
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
imageStyle.border = `2px solid ${green}`
|
||||
break
|
||||
} else {
|
||||
imageStyle.border = `3px solid ${red}`
|
||||
imageStyle.border = `2px solid ${red}`
|
||||
}
|
||||
|
||||
|
||||
@@ -3012,10 +3126,10 @@ const Workflows2 = (props) => {
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
imageStyle.border = `2px solid ${green}`
|
||||
break
|
||||
} else {
|
||||
imageStyle.border = `3px solid ${red}`
|
||||
imageStyle.border = `2px solid ${red}`
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3081,13 +3195,12 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
|
||||
//const isPublicWorkflow = data?.objectID === undefined || data?.objectID === null
|
||||
const foundImage = data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
|
||||
|
||||
const foundImage = currTab !== 2 && showWorkflowImages === false ? "" : data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
|
||||
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
|
||||
return (
|
||||
<div
|
||||
id={`workflowbox-${data.id}`}
|
||||
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, overflow: "hidden", }}
|
||||
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : "inherit", borderBottom: isDistributed || hasSuborgs ? `1px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily, overflow: "hidden", }}
|
||||
>
|
||||
|
||||
{foundImage?.length > 0 ?
|
||||
@@ -3140,26 +3253,7 @@ const Workflows2 = (props) => {
|
||||
style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme.typography?.fontFamily }}
|
||||
>
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
<div
|
||||
style={{ cursor: "" }}
|
||||
onClick={() => {
|
||||
//navigate("/admin")
|
||||
}}
|
||||
>
|
||||
{image?.includes("data:image") ?
|
||||
<img
|
||||
alt={orgName}
|
||||
src={image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
:
|
||||
image
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
|
||||
<Tooltip arrow
|
||||
onMouseEnter={() => {
|
||||
@@ -3228,7 +3322,7 @@ const Workflows2 = (props) => {
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
textAlign: "center",
|
||||
textAlign: "left",
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: 22,
|
||||
@@ -3239,8 +3333,9 @@ const Workflows2 = (props) => {
|
||||
fontWeight: 500,
|
||||
minWidth: 375,
|
||||
maxWidth: 375,
|
||||
margin: "auto",
|
||||
overflow: "hidden",
|
||||
|
||||
color: "rgba(255,255,255,0.85)",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
@@ -3431,14 +3526,13 @@ const Workflows2 = (props) => {
|
||||
style={{
|
||||
justifyContent: "left",
|
||||
overflow: "hidden",
|
||||
marginTop: 13,
|
||||
marginTop: 8,
|
||||
maxHeight: 35,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
|
||||
textAlign: "center",
|
||||
textAlign: "left",
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
margin: "auto",
|
||||
}}
|
||||
>
|
||||
{data.tags !== undefined && data.tags !== null
|
||||
@@ -3478,7 +3572,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: 80, right: 8, }}>
|
||||
<div style={{ position: "absolute", top: 100, right: 8, }}>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
@@ -3521,6 +3615,27 @@ const Workflows2 = (props) => {
|
||||
</Tooltip>
|
||||
: null}
|
||||
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
<div
|
||||
style={{ position: "absolute", top: 70, right: -2, cursor: "" }}
|
||||
onClick={() => {
|
||||
//navigate("/admin")
|
||||
}}
|
||||
>
|
||||
{image?.includes("data:image") ?
|
||||
<img
|
||||
alt={orgName}
|
||||
src={image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
:
|
||||
image
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
</Grid>
|
||||
|
||||
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
|
||||
@@ -5305,6 +5420,21 @@ const Workflows2 = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Show/Hide Workflow image" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showWorkflowImages ? "#1a1a1a" : theme.palette.text.primary, background: showWorkflowImages ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
|
||||
const newView = !showWorkflowImages
|
||||
localStorage.setItem("showWorkflowImages", newView)
|
||||
setShowWorkflowImages(!showWorkflowImages)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
|
||||
@@ -86,8 +86,6 @@ spec:
|
||||
value: kubernetes
|
||||
- name: IS_KUBERNETES
|
||||
value: "true"
|
||||
- name: SHUFFLE_SWARM_CONFIG
|
||||
value: run
|
||||
{{- if .Values.orborus.extraEnvVars }}
|
||||
{{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVars "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
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.9.30
|
||||
github.com/shuffle/shuffle-shared v0.9.52
|
||||
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.20 // indirect
|
||||
github.com/frikky/schemaless v0.0.24 // 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
|
||||
@@ -93,8 +93,6 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.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
|
||||
@@ -105,6 +103,7 @@ require (
|
||||
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/shuffle/opensearch-go/v4 v4.0.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
|
||||
@@ -70,20 +70,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y=
|
||||
@@ -142,8 +128,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.20 h1:S/A2pQcRN9qa2RnufvxwCeM06trjG0JLTF3urt1tFQI=
|
||||
github.com/frikky/schemaless v0.0.20/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY=
|
||||
github.com/frikky/schemaless v0.0.24 h1:1bNg96gm82yLbRlDS0haQH/96X3GlCAEuTCSasXIJZE=
|
||||
github.com/frikky/schemaless v0.0.24/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=
|
||||
@@ -209,7 +195,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -246,8 +231,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -295,10 +278,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M=
|
||||
github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo=
|
||||
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
|
||||
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
|
||||
github.com/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=
|
||||
@@ -328,8 +307,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.9.30 h1:3CYvNyD7sTxdxoZjTVrtaDqFvSWQRKAFGaga6rPGf8A=
|
||||
github.com/shuffle/shuffle-shared v0.9.30/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
|
||||
github.com/shuffle/opensearch-go/v4 v4.0.0 h1:Mh85CD1MwOgXiFFYlzS1llnvdqL3CztRdR1ZT/SLIjU=
|
||||
github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0=
|
||||
github.com/shuffle/shuffle-shared v0.9.51 h1:jOlFr0cdhPkJxRxlFHddBSIucm62T8e2DVM2PGDkahY=
|
||||
github.com/shuffle/shuffle-shared v0.9.51/go.mod h1:g03P/ZM8pUwBbdxW0l9A1siMMuemEQshgDpO9hF23eE=
|
||||
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=
|
||||
@@ -354,10 +335,19 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
|
||||
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -452,9 +442,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
@@ -497,13 +485,11 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
@@ -515,7 +501,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
|
||||
@@ -18,12 +18,14 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/shuffle/shuffle-shared"
|
||||
@@ -113,8 +115,8 @@ var memcached = os.Getenv("SHUFFLE_MEMCACHED")
|
||||
var queuePerMinute = os.Getenv("SHUFFLE_EXECUTION_PER_MINIUTE")
|
||||
var queuePerMinuteInt int
|
||||
|
||||
// For it to download from Sigma?
|
||||
var pipelineApikey = os.Getenv("SHUFFLE_PIPELINE_AUTH")
|
||||
// Used to download file categories. Not required since 2.1.1
|
||||
var pipelineApikey = ""
|
||||
var pipelineUrl = os.Getenv("SHUFFLE_PIPELINE_URL")
|
||||
|
||||
var executionIds = []string{}
|
||||
@@ -439,12 +441,32 @@ func deployServiceWorkers(image string) {
|
||||
if err != nil {
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "already exists") {
|
||||
// Try patching for attachable
|
||||
|
||||
if debug {
|
||||
log.Printf("[DEBUG] Network %s already exists", networkName)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] Failed to create network %s for workers: %s. This is not critical, and containers will still be added", networkName, err)
|
||||
}
|
||||
}
|
||||
|
||||
networkID := ""
|
||||
|
||||
// find network ID
|
||||
networks, err := dockercli.NetworkList(ctx, network.ListOptions{})
|
||||
if err == nil {
|
||||
for _, net := range networks {
|
||||
if net.Name == networkName {
|
||||
if net.Scope == "swarm" {
|
||||
log.Printf("[DEBUG] Found swarm-scoped network: %s (%s)", networkName, net.ID)
|
||||
networkID = net.ID
|
||||
} else {
|
||||
log.Printf("[WARNING] Network %s exists but is not swarm scoped (scope=%s)", networkName, net.Scope)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
isMemcachedRunning, err := checkMemcached(ctx, dockercli)
|
||||
if err != nil {
|
||||
@@ -461,12 +483,17 @@ func deployServiceWorkers(image string) {
|
||||
}
|
||||
*/
|
||||
|
||||
if networkID == "" {
|
||||
log.Printf("[ERROR] Network %s does not exist", networkName)
|
||||
networkID = networkName
|
||||
}
|
||||
|
||||
defaultNetworkAttach := false
|
||||
if containerId != "" {
|
||||
log.Printf("[DEBUG] Should connect orborus container to worker network as it's running in Docker with name %#v!", containerId)
|
||||
// https://pkg.go.dev/github.com/docker/docker@v20.10.12+incompatible/api/types/network#EndpointSettings
|
||||
networkConfig := &network.EndpointSettings{}
|
||||
err := dockercli.NetworkConnect(ctx, networkName, containerId, networkConfig)
|
||||
err := dockercli.NetworkConnect(ctx, networkID, containerId, networkConfig)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed connecting Orborus to docker network %s: %s", networkName, err)
|
||||
}
|
||||
@@ -489,7 +516,7 @@ func deployServiceWorkers(image string) {
|
||||
for _, container := range containers {
|
||||
if strings.Contains(strings.ToLower(container.Image), "docker-socket-proxy") {
|
||||
networkConfig := &network.EndpointSettings{}
|
||||
err := dockercli.NetworkConnect(ctx, networkName, container.ID, networkConfig)
|
||||
err := dockercli.NetworkConnect(ctx, networkID, container.ID, networkConfig)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed connecting Docker socket proxy to docker network %s: %s", networkName, err)
|
||||
} else {
|
||||
@@ -569,7 +596,7 @@ func deployServiceWorkers(image string) {
|
||||
},
|
||||
Networks: []swarm.NetworkAttachmentConfig{
|
||||
swarm.NetworkAttachmentConfig{
|
||||
Target: networkName,
|
||||
Target: networkID,
|
||||
},
|
||||
swarm.NetworkAttachmentConfig{
|
||||
Target: "ingress",
|
||||
@@ -738,7 +765,36 @@ func deployServiceWorkers(image string) {
|
||||
|
||||
if err == nil {
|
||||
log.Printf("[DEBUG] Successfully deployed workers with %d replica(s) on %d node(s)", replicas, cnt)
|
||||
// wait for service to be ready
|
||||
time.Sleep(time.Duration(rand.Intn(4)+1) * time.Second)
|
||||
|
||||
//log.Printf("[DEBUG] Servicecreate request: %#v %#v", service, err)
|
||||
// patch service network
|
||||
// this is an edgecase that we noticed on docker version 29
|
||||
// and API version 1.44
|
||||
services, serr := dockercli.ServiceList(ctx, types.ServiceListOptions{})
|
||||
if serr == nil {
|
||||
for _, svc := range services {
|
||||
if svc.Spec.Annotations.Name == innerContainerName {
|
||||
log.Printf("[DEBUG] Found service %s (%s) — patching network attach", innerContainerName, svc.ID)
|
||||
|
||||
spec := svc.Spec
|
||||
spec.TaskTemplate.Networks = append(spec.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{
|
||||
Target: networkID,
|
||||
})
|
||||
|
||||
_, uerr := dockercli.ServiceUpdate(ctx, svc.ID, svc.Version, spec, types.ServiceUpdateOptions{})
|
||||
if uerr != nil {
|
||||
log.Printf("[WARNING] Failed to patch service %s with network %s: %v", innerContainerName, networkID, uerr)
|
||||
} else {
|
||||
log.Printf("[INFO] Successfully attached network %s to service %s", networkID, innerContainerName)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WARNING] Failed to list services for patching network attach: %v", serr)
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(fmt.Sprintf("%s", err), "Already Exists") && !strings.Contains(fmt.Sprintf("%s", err), "is already in use by service") {
|
||||
log.Printf("[ERROR] Failed making service: %s", err)
|
||||
@@ -2075,7 +2131,10 @@ func sendRemoveRequest(client *http.Client, toBeRemoved shuffle.ExecutionRequest
|
||||
|
||||
resultResp, err := client.Do(result)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed making confirm request: %s", err)
|
||||
if !strings.Contains(fmt.Sprintf("%s", err), "timeout") {
|
||||
log.Printf("[ERROR] Failed making confirm request: %s", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
return err
|
||||
}
|
||||
@@ -2102,6 +2161,66 @@ func cleanup() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func StartAgent() {
|
||||
log.Printf("[INFO] Starting Orborus agent mode")
|
||||
|
||||
auditLogEnabled := os.Getenv("SHUFFLE_AUDIT_LOG_ENABLED") == "true"
|
||||
|
||||
if auditLogEnabled {
|
||||
log.Printf("[INFO] Audit log monitoring is enabled")
|
||||
|
||||
// Initialize telemetry configuration
|
||||
telemetryConfig := shuffle.TelemetryConfig{
|
||||
Enabled: true,
|
||||
Modes: []string{"audit_log"},
|
||||
BufferSize: 1000,
|
||||
FlushInterval: 10 * time.Second,
|
||||
}
|
||||
|
||||
if excludePatterns := os.Getenv("SHUFFLE_AUDIT_LOG_EXCLUDE"); excludePatterns != "" {
|
||||
patterns := strings.Split(excludePatterns, ",")
|
||||
telemetryConfig.Filters = append(telemetryConfig.Filters, shuffle.TelemetryFilter{
|
||||
Type: "message",
|
||||
Exclude: patterns,
|
||||
})
|
||||
}
|
||||
|
||||
if includePatterns := os.Getenv("SHUFFLE_AUDIT_LOG_INCLUDE"); includePatterns != "" {
|
||||
patterns := strings.Split(includePatterns, ",")
|
||||
telemetryConfig.Filters = append(telemetryConfig.Filters, shuffle.TelemetryFilter{
|
||||
Type: "message",
|
||||
Include: patterns,
|
||||
})
|
||||
}
|
||||
|
||||
collector, err := shuffle.NewAuditLogCollector(telemetryConfig)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to create audit log collector: %v", err)
|
||||
} else {
|
||||
ctx := context.Background()
|
||||
if err := collector.LogCollectorStart(ctx); err != nil {
|
||||
log.Printf("[ERROR] Failed to start audit log collector: %v", err)
|
||||
} else {
|
||||
log.Printf("[INFO] Audit log collector started successfully")
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Printf("[INFO] Received shutdown signal, stopping audit log collector...")
|
||||
collector.Stop()
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[INFO] Audit log monitoring is disabled")
|
||||
}
|
||||
select {}
|
||||
}
|
||||
|
||||
// Initial loop etc
|
||||
func main() {
|
||||
// Get arch. amd64 or arm64
|
||||
@@ -2110,6 +2229,25 @@ func main() {
|
||||
//signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
//defer cleanup()
|
||||
|
||||
agentMode := os.Getenv("SHUFFLE_AGENT_MODE")
|
||||
if agentMode == "true" {
|
||||
log.Printf("[INFO] Running in agent mode. Starting the agent.")
|
||||
StartAgent()
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getenv("SHUFFLE_PIPELINE_STANDALONE") == "true" {
|
||||
log.Printf("[INFO] Allowing use of standalone pipeline (tenzir). URL: %s", pipelineUrl)
|
||||
|
||||
//if os.Getenv("SHUFFLE_SKIP_PIPELINES") == "false" {
|
||||
// os.Setenv("SHUFFLE_SKIP_PIPELINES", "true")
|
||||
//}
|
||||
|
||||
//if os.Getenv("SHUFFLE_PIPELINE_ENABLED") == "true" {
|
||||
// os.Setenv("SHUFFLE_PIPELINE_ENABLED", "false")
|
||||
//}
|
||||
}
|
||||
|
||||
// Block until a signal is received
|
||||
if shuffle.IsRunningInCluster() {
|
||||
log.Printf("[INFO] Running inside k8s cluster")
|
||||
@@ -2931,15 +3069,18 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error {
|
||||
}
|
||||
|
||||
func deployTenzirNode() error {
|
||||
if os.Getenv("SHUFFLE_SKIP_PIPELINES") == "false" || os.Getenv("SHUFFLE_PIPELINE_ENABLED") == "true" {
|
||||
// return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
|
||||
//log.Printf("[INFO] Pipelines are enabled by user")
|
||||
} else {
|
||||
// Disabled all pipeline features
|
||||
if os.Getenv("SHUFFLE_SKIP_PIPELINES") != "true" {
|
||||
return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
|
||||
}
|
||||
|
||||
// Specifically for standalone tenzir
|
||||
if os.Getenv("SHUFFLE_PIPELINE_STANDALONE") == "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isKubernetes == "true" {
|
||||
return errors.New("Kubernetes not implemented for Tenzir node")
|
||||
return errors.New("Tenzir not implemented for k8s")
|
||||
}
|
||||
|
||||
err := checkTenzirNode()
|
||||
@@ -3145,12 +3286,21 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
||||
hostConfig.Mounts = []mount.Mount{}
|
||||
}
|
||||
|
||||
//networkingConfig := &network.NetworkingConfig{
|
||||
// EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
// "tenzir-network": {
|
||||
// IPAMConfig: &network.EndpointIPAMConfig{
|
||||
// IPv4Address: "192.168.102.100",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
//}
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
"tenzir-network": {
|
||||
IPAMConfig: &network.EndpointIPAMConfig{
|
||||
IPv4Address: "192.168.102.100",
|
||||
},
|
||||
IPAMConfig: nil,
|
||||
Aliases: []string{"tenzir-node"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -3205,7 +3355,7 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
||||
|
||||
log.Printf("[INFO] Successfully deployed Tenzir Node! Setting up default syslog listener on TCP/1514 AND UDP/1514")
|
||||
|
||||
command := `from "tcp://0.0.0.0:1514" { read_syslog } | import`
|
||||
command := `load_tcp "0.0.0.0:1514" { read_syslog } | import`
|
||||
_, err = createPipeline(command, "default-syslog-tcp-514")
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to create tcp syslog pipeline: %s", err)
|
||||
@@ -3805,12 +3955,14 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
|
||||
err := deployTenzirNode()
|
||||
if err != nil {
|
||||
if (!strings.Contains(err.Error(), "SHUFFLE_SKIP_PIPELINES") && !strings.Contains(err.Error(), "Kubernetes not implemented for Tenzir node")) && !strings.Contains(err.Error(), "Tenzir Node is already running") && !strings.Contains(err.Error(), "docker daemon") {
|
||||
|
||||
log.Printf("[ERROR] Tenzir node connection problem: %s", err)
|
||||
|
||||
} else {
|
||||
//tenzirDisabled = true
|
||||
log.Printf("[WARNING] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err)
|
||||
if debug {
|
||||
log.Printf("[WARNING] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return pipelinePayload, err
|
||||
@@ -4167,7 +4319,9 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
|
||||
newresp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Connection refused?
|
||||
log.Printf("[ERROR][%s] Error running worker request to %s (1): %s", workflowExecution.ExecutionId, streamUrl, err)
|
||||
if !strings.Contains(fmt.Sprintf("%s", err), "timeout") {
|
||||
log.Printf("[ERROR][%s] Error running worker request to %s (1): %s", workflowExecution.ExecutionId, streamUrl, err)
|
||||
}
|
||||
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "connection refused") || strings.Contains(fmt.Sprintf("%s", err), "EOF") {
|
||||
workerImage := fmt.Sprintf("ghcr.io/shuffle/shuffle-worker:%s", workerVersion)
|
||||
|
||||
@@ -11,8 +11,8 @@ require (
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.9.30
|
||||
github.com/shuffle/singul v0.0.16
|
||||
github.com/shuffle/shuffle-shared v0.9.52
|
||||
github.com/shuffle/singul v0.0.20
|
||||
k8s.io/api v0.33.1
|
||||
k8s.io/apimachinery v0.33.1
|
||||
k8s.io/client-go v0.33.1
|
||||
@@ -60,7 +60,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.20 // indirect
|
||||
github.com/frikky/schemaless v0.0.24 // 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,8 +95,6 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.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
|
||||
@@ -107,6 +105,7 @@ require (
|
||||
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/shuffle/opensearch-go/v4 v4.0.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
|
||||
@@ -2116,6 +2116,11 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
log.Printf("[WARNING] Stream result missing execution ID and authorization; injecting them from workflow execution")
|
||||
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
streamResultUrl,
|
||||
@@ -2550,6 +2555,11 @@ func getWorkerBackendExecution(auth string, executionId string) (*shuffle.Workfl
|
||||
return workflowExecution, err
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("[INFO] Here is the result we got back from backend: %s", workflowExecution.Results)
|
||||
}
|
||||
|
||||
setWorkflowExecution(context.Background(), *workflowExecution, false)
|
||||
return workflowExecution, nil
|
||||
}
|
||||
|
||||
@@ -3449,7 +3459,6 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
||||
_ = service
|
||||
|
||||
if err != nil {
|
||||
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "network") && strings.Contains(fmt.Sprintf("%s", err), "not found") {
|
||||
log.Printf("[DEBUG] Network %s not found. Trying to initialize it.", networkName)
|
||||
networkErr := initSwarmNetwork()
|
||||
@@ -3476,6 +3485,63 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
||||
|
||||
log.Printf("[DEBUG] Failed deploying %s with image %s: %s", name, image, err)
|
||||
return err
|
||||
} else {
|
||||
// wait for service to be ready
|
||||
time.Sleep(time.Duration(rand.Intn(4)+1) * time.Second)
|
||||
//log.Printf("[DEBUG] Servicecreate request: %#v %#v", service, err)
|
||||
// patch service network
|
||||
// this is an edgecase that we noticed on docker version 29
|
||||
// and API version 1.44
|
||||
|
||||
// get networkID of swarmNetworkName
|
||||
networkID := ""
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// find network ID
|
||||
networks, err := dockercli.NetworkList(ctx, network.ListOptions{})
|
||||
if err == nil {
|
||||
for _, net := range networks {
|
||||
if net.Name == networkName {
|
||||
if net.Scope == "swarm" {
|
||||
log.Printf("[DEBUG] Found swarm-scoped network: %s (%s)", networkName, net.ID)
|
||||
networkID = net.ID
|
||||
} else {
|
||||
log.Printf("[WARNING] Network %s exists but is not swarm scoped (scope=%s)", networkName, net.Scope)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if networkID == "" {
|
||||
log.Printf("[ERROR] Network %s not found", networkName)
|
||||
networkID = networkName
|
||||
}
|
||||
|
||||
services, serr := dockercli.ServiceList(ctx, types.ServiceListOptions{})
|
||||
if serr == nil {
|
||||
for _, svc := range services {
|
||||
if svc.ID == service.ID {
|
||||
log.Printf("[DEBUG] Found service %s (%s) — patching network attach", service.ID, svc.ID)
|
||||
|
||||
spec := svc.Spec
|
||||
spec.TaskTemplate.Networks = append(spec.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{
|
||||
Target: networkID,
|
||||
})
|
||||
|
||||
_, uerr := dockercli.ServiceUpdate(ctx, svc.ID, svc.Version, spec, types.ServiceUpdateOptions{})
|
||||
if uerr != nil {
|
||||
log.Printf("[WARNING] Failed to patch service %s with network %s: %v", service.ID, networkID, uerr)
|
||||
} else {
|
||||
log.Printf("[INFO] Successfully attached network %s to service %s", networkID, service.ID)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WARNING] Failed to list services for patching network attach: %v", serr)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Successfully deployed service %s with image %s on port %d", name, image, deployport)
|
||||
|
||||
Reference in New Issue
Block a user