@@ -24,13 +24,13 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install apt dependencies
|
- name: Setup Helm
|
||||||
run: |
|
uses: azure/setup-helm@v4
|
||||||
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
|
with:
|
||||||
sudo apt-get install apt-transport-https -y --no-install-recommends
|
version: v3.18.4 # I run it locally so I know it works :b
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
|
|
||||||
sudo apt-get update
|
- name: Verify Helm
|
||||||
sudo apt-get install helm -y --no-install-recommends
|
run: helm version
|
||||||
|
|
||||||
- name: Set versions
|
- name: Set versions
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Install apt dependencies
|
- name: Install apt dependencies
|
||||||
run: |
|
run: |
|
||||||
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
|
|
||||||
sudo apt-get install apt-transport-https -y --no-install-recommends
|
sudo apt-get install apt-transport-https -y --no-install-recommends
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
|
curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install helm -y --no-install-recommends
|
sudo apt-get install helm -y --no-install-recommends
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ go 1.24.0
|
|||||||
|
|
||||||
toolchain go1.24.3
|
toolchain go1.24.3
|
||||||
|
|
||||||
|
//replace github.com/shuffle/opensearch-go => ../../../opensearch-go
|
||||||
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
|
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
|
||||||
|
|
||||||
//replace github.com/frikky/schemaless => ../../../schemaless
|
//replace github.com/frikky/schemaless => ../../../schemaless
|
||||||
|
|
||||||
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
||||||
@@ -24,8 +26,8 @@ require (
|
|||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/shuffle/shuffle-shared v0.9.25
|
github.com/shuffle/shuffle-shared v0.9.34
|
||||||
github.com/shuffle/singul v0.0.16
|
github.com/shuffle/singul v0.0.17
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
google.golang.org/api v0.236.0
|
google.golang.org/api v0.236.0
|
||||||
google.golang.org/grpc v1.72.2
|
google.golang.org/grpc v1.72.2
|
||||||
@@ -73,7 +75,7 @@ require (
|
|||||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/frikky/schemaless v0.0.20 // indirect
|
github.com/frikky/schemaless v0.0.22 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // 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-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/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 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0=
|
||||||
github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4=
|
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.22 h1:aMc7cc/lr1zpogjGWbY0j6J2f6QqyfPbbW6Y9JgTAqE=
|
||||||
github.com/frikky/schemaless v0.0.20/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY=
|
github.com/frikky/schemaless v0.0.22/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 h1:FMoLq+Zhv9Oz/rFmu6JWkImfr6CBgZOPcL+bHW4gS0o=
|
||||||
github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY=
|
github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
@@ -363,10 +363,10 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
|
|||||||
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
github.com/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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shuffle/shuffle-shared v0.9.21 h1:ZxA/g0Lk93miHJpVYAhtTQmtWLJWcCtm6kFNF772Ynk=
|
github.com/shuffle/shuffle-shared v0.9.34 h1:4RtGRMoYtBQia2CLbuH7aPFM+EMFXbH0ccIQsBXs8n8=
|
||||||
github.com/shuffle/shuffle-shared v0.9.21/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
|
github.com/shuffle/shuffle-shared v0.9.34/go.mod h1:WbLPLyBrh86ieRBgDyjC43M2qBF8a7E8jO8Pv7t8SOk=
|
||||||
github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E=
|
github.com/shuffle/singul v0.0.17 h1:mxaPtj6z85Nf6tl7L2gwDliTfEZtRQqApuu9iKcP75o=
|
||||||
github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs=
|
github.com/shuffle/singul v0.0.17/go.mod h1:8c42n1NahhCIPxzLxwp9eYbWkvY4+ct0jfbhkRsRRsY=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
|||||||
+192
-15
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/shuffle/shuffle-shared"
|
"github.com/shuffle/shuffle-shared"
|
||||||
"github.com/shuffle/singul/pkg"
|
"github.com/shuffle/singul/pkg"
|
||||||
|
|
||||||
|
"net/http/pprof"
|
||||||
|
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -46,6 +48,7 @@ import (
|
|||||||
newscheduler "github.com/carlescere/scheduler"
|
newscheduler "github.com/carlescere/scheduler"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"sort"
|
||||||
|
|
||||||
// Web
|
// Web
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -67,11 +70,13 @@ var debug = false
|
|||||||
//var syncUrl = "http://localhost:5002"
|
//var syncUrl = "http://localhost:5002"
|
||||||
|
|
||||||
type retStruct struct {
|
type retStruct struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
SyncFeatures shuffle.SyncFeatures `json:"sync_features"`
|
SyncFeatures shuffle.SyncFeatures `json:"sync_features"`
|
||||||
SessionKey string `json:"session_key"`
|
SessionKey string `json:"session_key"`
|
||||||
IntervalSeconds int64 `json:"interval_seconds"`
|
IntervalSeconds int64 `json:"interval_seconds"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
|
Subscriptions []shuffle.PaymentSubscription `json:"subscriptions"`
|
||||||
|
Licensed bool `json:"licensed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Contact struct {
|
type Contact struct {
|
||||||
@@ -933,6 +938,78 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
log.Printf("[DEBUG] Failed to get org during getinfo: %s", err)
|
log.Printf("[DEBUG] Failed to get org during getinfo: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
childOrgs := []shuffle.Org{}
|
||||||
|
if len(org.CreatorOrg) > 0 {
|
||||||
|
childOrgs, err = shuffle.GetAllChildOrgs(ctx, org.CreatorOrg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to get child orgs during getinfo: %s", err)
|
||||||
|
childOrgs = []shuffle.Org{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failToLoadOrgs := []string{}
|
||||||
|
sort.Slice(childOrgs, func(i, j int) bool {
|
||||||
|
return childOrgs[i].Created < childOrgs[j].Created
|
||||||
|
})
|
||||||
|
|
||||||
|
parentOrg := &shuffle.Org{}
|
||||||
|
if len(org.CreatorOrg) > 0 {
|
||||||
|
parentOrg, err = shuffle.GetOrg(ctx, org.CreatorOrg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to get parent org during getinfo: %s", err)
|
||||||
|
parentOrg = &shuffle.Org{}
|
||||||
|
}
|
||||||
|
licenseOrg := shuffle.HandleCheckLicense(ctx, *parentOrg)
|
||||||
|
parentOrg = &licenseOrg
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 3
|
||||||
|
|
||||||
|
if parentOrg.Licensed && parentOrg.SyncFeatures.MultiTenant.Active {
|
||||||
|
limit = int(parentOrg.SyncFeatures.MultiTenant.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, org := range childOrgs {
|
||||||
|
|
||||||
|
if index < limit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
failToLoadOrgs = append(failToLoadOrgs, org.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(org.CreatorOrg) > 0 && len(childOrgs) > limit && shuffle.ArrayContains(failToLoadOrgs, userInfo.ActiveOrg.Id) {
|
||||||
|
|
||||||
|
userInfo.ActiveOrg = shuffle.OrgMini{
|
||||||
|
Id: parentOrg.Id,
|
||||||
|
Name: parentOrg.Name,
|
||||||
|
Role: userInfo.Role,
|
||||||
|
Branding: parentOrg.Branding,
|
||||||
|
Image: parentOrg.Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentOrg.Licensed {
|
||||||
|
log.Printf("[INFO] Parent org %s is licensed. But Multi-tenant feature is not active. Moving user %s to parent org %s", parentOrg.Name, userInfo.Username)
|
||||||
|
} else {
|
||||||
|
log.Printf("[INFO] Parent org %s has more than 3 child orgs and is not licensed. Moving user %s to parent org %s", parentOrg.Name, userInfo.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = shuffle.SetUser(ctx, &userInfo, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARNING] Failed setting user to parent org: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := "Parent org has more than 3 child orgs and is not licensed. Moving user to parent org. Contact support@shuffler.io for more information"
|
||||||
|
|
||||||
|
if parentOrg.Licensed {
|
||||||
|
reason = fmt.Sprintf("Parent organization is licensed, but the maximum number of sub-organizations (%d) has been reached. You have been moved to the parent organization. Please contact support@shuffler.io for further assistance.", parentOrg.SyncFeatures.MultiTenant.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
resp.Write([]byte(`{"success": true, "reason": "` + reason + `", "switch_parent": true}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//if err == nil {
|
//if err == nil {
|
||||||
if len(org.Id) > 0 {
|
if len(org.Id) > 0 {
|
||||||
if userInfo.Role == "" {
|
if userInfo.Role == "" {
|
||||||
@@ -1082,7 +1159,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
// Check for licensing/branding of parent and override
|
// Check for licensing/branding of parent and override
|
||||||
parentOrg, err := shuffle.GetOrg(ctx, parentOrgId)
|
parentOrg, err := shuffle.GetOrg(ctx, parentOrgId)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if parentOrg.LeadInfo.IntegrationPartner {
|
parent := shuffle.HandleCheckLicense(ctx, *parentOrg)
|
||||||
|
if parent.SyncFeatures.Branding.Active {
|
||||||
parsedStatus = append(parsedStatus, "integration_partner")
|
parsedStatus = append(parsedStatus, "integration_partner")
|
||||||
|
|
||||||
// except theme take from parent org
|
// except theme take from parent org
|
||||||
@@ -1120,11 +1198,15 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
userInfo.ActiveOrg.Branding = shuffle.OrgBranding{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// for parent org branding
|
// for parent org branding
|
||||||
if org.LeadInfo.IntegrationPartner {
|
licenseOrg := shuffle.HandleCheckLicense(ctx, *org)
|
||||||
|
org = &licenseOrg
|
||||||
|
if org.SyncFeatures.Branding.Active {
|
||||||
userInfo.ActiveOrg.Branding.Theme = org.Branding.Theme
|
userInfo.ActiveOrg.Branding.Theme = org.Branding.Theme
|
||||||
userInfo.ActiveOrg.Branding.DocumentationLink = org.Defaults.DocumentationReference
|
userInfo.ActiveOrg.Branding.DocumentationLink = org.Defaults.DocumentationReference
|
||||||
userInfo.ActiveOrg.Branding.SupportEmail = org.Branding.SupportEmail
|
userInfo.ActiveOrg.Branding.SupportEmail = org.Branding.SupportEmail
|
||||||
@@ -1133,6 +1215,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
userInfo.ActiveOrg.Branding.BrandName = org.Branding.BrandName
|
userInfo.ActiveOrg.Branding.BrandName = org.Branding.BrandName
|
||||||
|
|
||||||
parsedStatus = append(parsedStatus, "integration_partner")
|
parsedStatus = append(parsedStatus, "integration_partner")
|
||||||
|
} else {
|
||||||
|
userInfo.ActiveOrg.Branding = shuffle.OrgBranding{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,7 +1282,6 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
count := len(users)
|
count := len(users)
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
log.Printf("[WARNING] No users - redirecting for management user")
|
log.Printf("[WARNING] No users - redirecting for management user")
|
||||||
resp.WriteHeader(200)
|
resp.WriteHeader(200)
|
||||||
@@ -2061,7 +2144,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(hook.Workflows) == 1 {
|
if len(hook.Workflows) == 1 {
|
||||||
workflow, err := shuffle.GetWorkflow(ctx, hook.Workflows[0])
|
workflow, err := shuffle.GetWorkflow(ctx, hook.Workflows[0], true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, branch := range workflow.Branches {
|
for _, branch := range workflow.Branches {
|
||||||
if branch.SourceID == hook.Id {
|
if branch.SourceID == hook.Id {
|
||||||
@@ -3816,9 +3899,12 @@ func handleCloudJob(job shuffle.CloudSyncJob) error {
|
|||||||
// Handles jobs from remote (cloud)
|
// Handles jobs from remote (cloud)
|
||||||
func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
||||||
type retStruct struct {
|
type retStruct struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Jobs []shuffle.CloudSyncJob `json:"jobs"`
|
Jobs []shuffle.CloudSyncJob `json:"jobs"`
|
||||||
|
SyncFeatures shuffle.SyncFeatures `json:"sync_features"`
|
||||||
|
Subscriptions []shuffle.PaymentSubscription `json:"subscriptions"`
|
||||||
|
Licensed bool `json:"licensed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData := retStruct{}
|
responseData := retStruct{}
|
||||||
@@ -3885,6 +3971,31 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
|||||||
log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs))
|
log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("org_sync_features_%s", org.Id)
|
||||||
|
featuresBytes, err := json.Marshal(responseData.SyncFeatures)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to marshal SyncFeatures for cache: %s", err)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
for _, job := range responseData.Jobs {
|
for _, job := range responseData.Jobs {
|
||||||
err = handleCloudJob(job)
|
err = handleCloudJob(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4550,6 +4661,45 @@ func runInitEs(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self-cleaning
|
||||||
|
go func() {
|
||||||
|
cursor := ""
|
||||||
|
cnt := 0
|
||||||
|
newCtx := context.Background()
|
||||||
|
for _, org := range activeOrgs {
|
||||||
|
if len(org.Id) == 0 {
|
||||||
|
log.Printf("[DEBUG] No ID found for org with name '%s'. Why was it made?", org.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Starting self-cleanup of cache keys for org %s", org.Id)
|
||||||
|
|
||||||
|
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)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if newCursor == cursor || len(newCursor) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = newCursor
|
||||||
|
cnt += 1
|
||||||
|
if cnt > 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Finished self-cleanup of cache keys for org %s", org.Id)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
log.Printf("[INFO] Finished INIT (ES)")
|
log.Printf("[INFO] Finished INIT (ES)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4648,6 +4798,7 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error)
|
|||||||
org.CloudSync = false
|
org.CloudSync = false
|
||||||
org.SyncFeatures = shuffle.SyncFeatures{}
|
org.SyncFeatures = shuffle.SyncFeatures{}
|
||||||
org.SyncConfig = shuffle.SyncConfig{}
|
org.SyncConfig = shuffle.SyncConfig{}
|
||||||
|
org.Subscriptions = []shuffle.PaymentSubscription{}
|
||||||
|
|
||||||
err = shuffle.SetOrg(ctx, org, org.Id)
|
err = shuffle.SetOrg(ctx, org, org.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4863,8 +5014,6 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Respbody from sync: %s", string(respBody))
|
|
||||||
|
|
||||||
responseData := retStruct{}
|
responseData := retStruct{}
|
||||||
err = json.Unmarshal(respBody, &responseData)
|
err = json.Unmarshal(respBody, &responseData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4890,7 +5039,15 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
// 2. Add iterative sync schedule for interval seconds
|
// 2. Add iterative sync schedule for interval seconds
|
||||||
// 3. Add another environment for the org's users
|
// 3. Add another environment for the org's users
|
||||||
org.CloudSync = true
|
org.CloudSync = true
|
||||||
org.SyncFeatures = responseData.SyncFeatures
|
|
||||||
|
// set cache here for 30 min
|
||||||
|
cacheKey := fmt.Sprintf("org_sync_features_%s", org.Id)
|
||||||
|
featuresBytes, err := json.Marshal(responseData.SyncFeatures)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to marshal SyncFeatures for cache: %s", err)
|
||||||
|
} else {
|
||||||
|
shuffle.SetCache(ctx, cacheKey, featuresBytes, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
org.SyncConfig = shuffle.SyncConfig{
|
org.SyncConfig = shuffle.SyncConfig{
|
||||||
Apikey: responseData.SessionKey,
|
Apikey: responseData.SessionKey,
|
||||||
@@ -5362,6 +5519,11 @@ func initHandlers() {
|
|||||||
r.HandleFunc("/api/v2/workflows/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v2/workflows/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v2/workflows/generate/llm", shuffle.HandleWorkflowGenerationResponse).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v2/workflows/generate/llm", shuffle.HandleWorkflowGenerationResponse).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/api/v2/workflows/edit/llm", shuffle.HandleEditWorkflowWithLLM).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v2/workflows/edit/llm", shuffle.HandleEditWorkflowWithLLM).Methods("POST", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v2/workflows/generate", shuffle.GenerateSingulWorkflows).Methods("POST", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v2/datastore", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v2/datastore", shuffle.HandleSetDatastoreKey).Methods("POST", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v2/datastore/category/{category_key}", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v2/datastore/automate", shuffle.HandleDatastoreCategoryConfig).Methods("POST", "OPTIONS")
|
||||||
|
|
||||||
// New for recommendations in Shuffle
|
// New for recommendations in Shuffle
|
||||||
r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS")
|
||||||
@@ -5456,6 +5618,10 @@ func initHandlers() {
|
|||||||
r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
|
||||||
|
|
||||||
|
r.HandleFunc("/api/v1/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v1/stats", shuffle.HandleAppendStatistics).Methods("POST", "OPTIONS")
|
||||||
|
r.HandleFunc("/api/v1/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
|
||||||
|
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
|
||||||
@@ -5511,6 +5677,17 @@ func initHandlers() {
|
|||||||
r.HandleFunc("/api/v1/dashboards/{key}/widgets", shuffle.HandleNewWidget).Methods("POST", "OPTIONS")
|
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")
|
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") {
|
||||||
|
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)
|
||||||
|
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||||
|
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||||
|
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] Memory debugging is disabled. To enable, set SHUFFLE_DEBUG_MEMORY or DEBUG_MEMORY to true")
|
||||||
|
}
|
||||||
|
|
||||||
r.Use(shuffle.RequestMiddleware)
|
r.Use(shuffle.RequestMiddleware)
|
||||||
http.Handle("/", r)
|
http.Handle("/", r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque
|
|||||||
resp.Write([]byte(`{"success": true}`))
|
resp.Write([]byte(`{"success": true}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Authenticate this one? Can org ID be auth enough?
|
// FIXME: Authenticate this one. Can org ID be auth enough?
|
||||||
// (especially since we have a default: shuffle)
|
// (especially since we have a default: shuffle)
|
||||||
func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||||
cors := shuffle.HandleCors(resp, request)
|
cors := shuffle.HandleCors(resp, request)
|
||||||
@@ -325,7 +325,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
|||||||
orgId = env.OrgId
|
orgId = env.OrgId
|
||||||
}
|
}
|
||||||
|
|
||||||
executionRequests, err := shuffle.GetWorkflowQueue(ctx, environment, 100)
|
executionRequests, err := shuffle.GetWorkflowQueue(ctx, environment, 100, *env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.WriteHeader(500)
|
resp.WriteHeader(500)
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||||
@@ -336,47 +336,11 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
|||||||
if len(executionRequests.Data) == 0 {
|
if len(executionRequests.Data) == 0 {
|
||||||
executionRequests.Data = []shuffle.ExecutionRequest{}
|
executionRequests.Data = []shuffle.ExecutionRequest{}
|
||||||
} else {
|
} 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(env.Id) == 0 && len(env.Name) == 0 {
|
|
||||||
timeNow := int64(time.Now().Unix())
|
|
||||||
foundId := ""
|
|
||||||
for _, requestData := range executionRequests.Data {
|
|
||||||
execution, err := shuffle.GetWorkflowExecution(ctx, requestData.ExecutionId)
|
|
||||||
if err == nil {
|
|
||||||
if len(execution.ExecutionOrg) > 0 {
|
|
||||||
foundId = execution.ExecutionOrg
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(environment) > 0 {
|
|
||||||
|
|
||||||
env, err := shuffle.GetEnvironment(ctx, environment, foundId)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARNING] No env found matching %s - continuing without updating orborus anyway: %s", environment, err)
|
|
||||||
//resp.WriteHeader(401)
|
|
||||||
//resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No env found matching %s"}`, id)))
|
|
||||||
//return
|
|
||||||
} else {
|
|
||||||
if timeNow > env.Edited+60 {
|
|
||||||
env.RunningIp = request.RemoteAddr
|
|
||||||
env.Checkin = timeNow
|
|
||||||
err = shuffle.SetEnvironment(ctx, env)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARNING] Failed updating environment: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if len(executionRequests.Data) > 50 {
|
if len(executionRequests.Data) > 50 {
|
||||||
executionRequests.Data = executionRequests.Data[0:49]
|
executionRequests.Data = executionRequests.Data[0:49]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newjson, err := json.Marshal(executionRequests)
|
newjson, err := json.Marshal(executionRequests)
|
||||||
@@ -1847,13 +1811,12 @@ func getWorkflowApps(resp http.ResponseWriter, request *http.Request) {
|
|||||||
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
|
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err)
|
log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(400)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newapps := workflowapps
|
newapps := workflowapps
|
||||||
|
|
||||||
if len(user.PrivateApps) > 0 {
|
if len(user.PrivateApps) > 0 {
|
||||||
found := false
|
found := false
|
||||||
for _, item := range user.PrivateApps {
|
for _, item := range user.PrivateApps {
|
||||||
|
|||||||
@@ -91,6 +91,27 @@ services:
|
|||||||
- shuffle
|
- shuffle
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# OPTIONAL: advanced monitoring with cAdvisor
|
||||||
|
# If you have a seperate orborus config YAML on another server,
|
||||||
|
# Please add it there as well if you want to monitor that server too.
|
||||||
|
# FYI: both servers will show up on different dashboards.
|
||||||
|
# cadvisor:
|
||||||
|
# image: gcr.io/cadvisor/cadvisor:latest
|
||||||
|
# volumes:
|
||||||
|
# - /:/rootfs:ro
|
||||||
|
# - /var/run:/var/run:ro
|
||||||
|
# - /sys:/sys:ro
|
||||||
|
# - /var/lib/docker/:/var/lib/docker:ro
|
||||||
|
# - /dev/disk/:/dev/disk:ro
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
# privileged: true
|
||||||
|
# devices:
|
||||||
|
# - /dev/kmsg:/dev/kmsg
|
||||||
|
# networks:
|
||||||
|
# - shuffle
|
||||||
|
|
||||||
#memcached:
|
#memcached:
|
||||||
# image: memcached:latest
|
# image: memcached:latest
|
||||||
# container_name: shuffle-cache
|
# container_name: shuffle-cache
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import AgentUI from "./views/AgentUI.jsx";
|
|||||||
import Welcome from "./views/Welcome.jsx";
|
import Welcome from "./views/Welcome.jsx";
|
||||||
import Dashboard from "./views/Dashboard.jsx";
|
import Dashboard from "./views/Dashboard.jsx";
|
||||||
import DashboardView from "./views/DashboardViews.jsx";
|
import DashboardView from "./views/DashboardViews.jsx";
|
||||||
|
import NewDashboard from "./views/NewDashboard.jsx";
|
||||||
import AdminSetup from "./views/AdminSetup.jsx";
|
import AdminSetup from "./views/AdminSetup.jsx";
|
||||||
import Admin from "./views/Admin.jsx";
|
import Admin from "./views/Admin.jsx";
|
||||||
import Docs from "./views/Docs.jsx";
|
import Docs from "./views/Docs.jsx";
|
||||||
@@ -167,6 +168,47 @@ const App = (message, props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userdata &&
|
||||||
|
userdata?.org_status?.includes("integration_partner") &&
|
||||||
|
userdata?.active_org?.image) {
|
||||||
|
|
||||||
|
const existingLinks = document.querySelectorAll("link[rel*='icon']");
|
||||||
|
existingLinks.forEach(link => link.parentNode.removeChild(link));
|
||||||
|
const newLink = document.createElement('link');
|
||||||
|
newLink.rel = 'icon';
|
||||||
|
newLink.type = 'image/x-icon';
|
||||||
|
try {
|
||||||
|
if (typeof userdata.active_org.image === 'string') {
|
||||||
|
if (userdata.active_org.image.startsWith('data:')) {
|
||||||
|
newLink.href = userdata.active_org.image;
|
||||||
|
} else if (userdata.active_org.image.startsWith('/')) {
|
||||||
|
newLink.href = userdata.active_org.image;
|
||||||
|
} else {
|
||||||
|
let mimeType = 'image/png';
|
||||||
|
newLink.href = `data:${mimeType};base64,${userdata.active_org.image}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Image data is not in expected format:", userdata.active_org.image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new favicon link to head
|
||||||
|
document.head.appendChild(newLink);
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
iframe.contentDocument.write('<link rel="icon" href="' + newLink.href + '">');
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating favicon:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [userdata]);
|
||||||
|
|
||||||
const checkLogin = () => {
|
const checkLogin = () => {
|
||||||
var baseurl = globalUrl;
|
var baseurl = globalUrl;
|
||||||
fetch(`${globalUrl}/api/v1/getinfo`, {
|
fetch(`${globalUrl}/api/v1/getinfo`, {
|
||||||
@@ -180,6 +222,13 @@ const App = (message, props) => {
|
|||||||
var userInfo = {};
|
var userInfo = {};
|
||||||
if (responseJson.success === true) {
|
if (responseJson.success === true) {
|
||||||
//console.log("USER: ", responseJson);
|
//console.log("USER: ", responseJson);
|
||||||
|
if (responseJson?.switch_parent === true) {
|
||||||
|
toast.info(responseJson.reason)
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 3000);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userInfo = responseJson;
|
userInfo = responseJson;
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
@@ -824,6 +873,21 @@ const App = (message, props) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/new-dashboard"
|
||||||
|
element={
|
||||||
|
<NewDashboard
|
||||||
|
serverside={false}
|
||||||
|
isLoaded={isLoaded}
|
||||||
|
isLoggedIn={isLoggedIn}
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
userdata={userdata}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
@import url('https://fonts.googleapis.com/css?family=Nunito+Sans');
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: "Nunito Sans", sans-serif;
|
font-family: "Segoe UI", Roboto, "Noto Sans", "Liberation Sans", Arial, "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Nunito Sans", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext, memo } from 'react';
|
import React, { useState, useEffect, useContext, memo } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import OrganizationTab from '../components/OrganizationTab.jsx';
|
import OrganizationTab from '../components/OrganizationTab.jsx';
|
||||||
import PartnerTab from '../components/PartnerTab.jsx';
|
import PartnerTab from '../components/PartnerTab.jsx';
|
||||||
import UserManagmentTab from '../components/UserManagmentTab.jsx';
|
import UserManagmentTab from '../components/UserManagmentTab.jsx';
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
||||||
GroupOutlined as GroupOutlinedIcon
|
GroupOutlined as GroupOutlinedIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
import theme, { getTheme } from '../theme.jsx';
|
import theme, { getTheme } from '../theme.jsx';
|
||||||
import { Button, Skeleton, Tooltip } from '@mui/material';
|
import { Button, Skeleton, Tooltip } from '@mui/material';
|
||||||
import { Index } from 'react-instantsearch-dom';
|
import { Index } from 'react-instantsearch-dom';
|
||||||
|
|||||||
@@ -992,16 +992,16 @@ const AppAuthTab = memo((props) => {
|
|||||||
<Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography>
|
<Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography>
|
||||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}>
|
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}>
|
||||||
<Typography variant='body2' color="textSecondary">
|
<Typography variant='body2' color="textSecondary">
|
||||||
Control the authentication options for individual apps.
|
Control the authentication options for individual apps. These keys are write-only, and cannot be viewed after creation. If you want editable secrets (e.g. for use in code), use <a href="admin?tab=datastore&category=protected" style={{ color: theme.palette.linkColor }}>Protected Keys</a>.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="/docs/organizations#app_authentication"
|
href="/docs/organizations#app_authentication"
|
||||||
style={{ color: theme.palette.linkColor }}
|
style={{ minWidth: 200, marginleft: 25, color: theme.palette.linkColor }}
|
||||||
>
|
>
|
||||||
Learn more about App Authentication
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1787,7 +1787,7 @@ const Hits = ({
|
|||||||
|
|
||||||
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
|
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
|
||||||
setAuthenticationType({
|
setAuthenticationType({
|
||||||
type: "",
|
type: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
selectedAppData.authentication = {
|
selectedAppData.authentication = {
|
||||||
@@ -1955,6 +1955,7 @@ const Hits = ({
|
|||||||
if (data === undefined || data === null) {
|
if (data === undefined || data === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
|
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
|
||||||
if (filteredData.length === 0) {
|
if (filteredData.length === 0) {
|
||||||
setAppAuthentication([]);
|
setAppAuthentication([]);
|
||||||
@@ -1965,7 +1966,7 @@ const Hits = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const HandleAppAuthentication = ()=>{
|
const HandleAppAuthentication = () => {
|
||||||
|
|
||||||
const url = `${globalUrl}/api/v1/apps/authentication`;
|
const url = `${globalUrl}/api/v1/apps/authentication`;
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
|||||||
import CreateIcon from '@mui/icons-material/Create'
|
import CreateIcon from '@mui/icons-material/Create'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
import Dropzone from "./Dropzone.jsx";
|
||||||
|
|
||||||
const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenApi = false, prefillOpenApiData = "" }) => {
|
||||||
const [openApiModal, setOpenApiModal] = useState(false)
|
const [openApiModal, setOpenApiModal] = useState(false)
|
||||||
const [generateAppModal, setGenerateAppModal] = useState(false)
|
const [generateAppModal, setGenerateAppModal] = useState(false)
|
||||||
const [openApi, setOpenApi] = useState("")
|
const [openApi, setOpenApi] = useState("")
|
||||||
@@ -35,6 +36,16 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const upload = useRef()
|
const upload = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && (startOpenApi || (prefillOpenApiData && prefillOpenApiData.length > 0))) {
|
||||||
|
if (prefillOpenApiData && prefillOpenApiData.length > 0) {
|
||||||
|
setOpenApiData(prefillOpenApiData)
|
||||||
|
setIsDropzone(true)
|
||||||
|
}
|
||||||
|
setOpenApiModal(true)
|
||||||
|
}
|
||||||
|
}, [open, startOpenApi, prefillOpenApiData])
|
||||||
|
|
||||||
// Style for the create options
|
// Style for the create options
|
||||||
const AppCreateButton = ({ text, func, icon }) => {
|
const AppCreateButton = ({ text, func, icon }) => {
|
||||||
const [hover, setHover] = React.useState(false)
|
const [hover, setHover] = React.useState(false)
|
||||||
@@ -467,6 +478,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Dropzone onDrop={uploadFile} style={{ width: '100%' }}>
|
||||||
<DialogTitle sx={{
|
<DialogTitle sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@@ -504,12 +516,15 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
|||||||
<Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}>
|
<Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}>
|
||||||
Paste in the URI for the OpenAPI or find out
|
Paste in the URI for the OpenAPI or find out
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link style={{
|
<Link
|
||||||
|
to="https://shuffler.io/docs/apps#getting-started"
|
||||||
|
style={{
|
||||||
color: '#ff8544',
|
color: '#ff8544',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontFamily: theme?.typography?.fontFamily
|
fontFamily: theme?.typography?.fontFamily,
|
||||||
|
textUnderlineOffset: "3px",
|
||||||
}}>
|
}}>
|
||||||
How to find URI for openAPI?
|
How to find URI for openAPI?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -568,31 +583,54 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
|||||||
Must point to a version 2 or 3 OpenAPI specification.
|
Must point to a version 2 or 3 OpenAPI specification.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography sx={{ mb: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}>
|
<Typography sx={{ mb: 1.5, mt: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}>
|
||||||
Or upload a YAML or JSON specification
|
Or upload a YAML or JSON specification
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<div style={{
|
||||||
<Button
|
width: '100%',
|
||||||
variant="outlined"
|
boxSizing: 'border-box',
|
||||||
|
border: '1px dashed rgba(255,255,255,0.35)',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
marginTop: '12px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
onClick={() => upload.current.click()}
|
onClick={() => upload.current.click()}
|
||||||
sx={{
|
|
||||||
color: '#FF8544',
|
|
||||||
borderColor: '#FF8544',
|
|
||||||
px: 5,
|
|
||||||
py: 1,
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: '#FF8544',
|
|
||||||
color: '#FF8544',
|
|
||||||
bgcolor: 'rgba(255,133,68,0.1)'
|
|
||||||
},
|
|
||||||
textTransform: 'none',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontFamily: theme?.typography?.fontFamily,
|
|
||||||
height: '40px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Upload
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
</Button>
|
<Typography sx={{ color: 'rgba(255,255,255,0.9)', fontSize: '15px', fontFamily: theme?.typography?.fontFamily }}>
|
||||||
|
Drag & drop your OpenAPI (YAML/JSON) anywhere
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'rgba(255,255,255,0.6)', fontSize: '13px', mt: 0.5, fontFamily: theme?.typography?.fontFamily }}>
|
||||||
|
or click to browse files
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={(e) => { e.stopPropagation(); upload.current.click(); }}
|
||||||
|
sx={{
|
||||||
|
color: '#FF8544',
|
||||||
|
borderColor: '#FF8544',
|
||||||
|
px: 3,
|
||||||
|
py: 0.75,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#FF8544',
|
||||||
|
color: '#FF8544',
|
||||||
|
bgcolor: 'rgba(255,133,68,0.1)'
|
||||||
|
},
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: theme?.typography?.fontFamily,
|
||||||
|
height: '36px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
hidden
|
hidden
|
||||||
@@ -638,6 +676,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
|||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
</Dropzone>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Generate App Modal */}
|
{/* Generate App Modal */}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
ClearRefinements,
|
ClearRefinements,
|
||||||
connectStateResults
|
connectStateResults
|
||||||
} from "react-instantsearch-dom";
|
} from "react-instantsearch-dom";
|
||||||
|
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||||
|
|
||||||
import aa from "search-insights";
|
import aa from "search-insights";
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -160,6 +161,8 @@ const AppGrid = (props) => {
|
|||||||
refine(searchQuery.trim());
|
refine(searchQuery.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form noValidate action="" role="search">
|
<form noValidate action="" role="search">
|
||||||
<TextField
|
<TextField
|
||||||
@@ -229,9 +232,10 @@ const AppGrid = (props) => {
|
|||||||
placeholder="Search more than 2500 Apps"
|
placeholder="Search more than 2500 Apps"
|
||||||
id="shuffle_search_field"
|
id="shuffle_search_field"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setSearchQuery(event.currentTarget.value);
|
const value = event.currentTarget.value;
|
||||||
|
setSearchQuery(value);
|
||||||
removeQuery("q");
|
removeQuery("q");
|
||||||
refine(event.currentTarget.value);
|
debouncedRefine(value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if(event.key === "Enter") {
|
if(event.key === "Enter") {
|
||||||
|
|||||||
@@ -0,0 +1,891 @@
|
|||||||
|
import React, { useState, useEffect, useContext, memo } from "react";
|
||||||
|
import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
|
||||||
|
import { getTheme } from "../theme.jsx";
|
||||||
|
import { isMobile } from "react-device-detect"
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
import PaperComponent from "../components/PaperComponent.jsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { v4 as uuidv4} from "uuid";
|
||||||
|
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Edit as EditIcon,
|
||||||
|
DragIndicator as DragIndicatorIcon,
|
||||||
|
Close as CloseIcon,
|
||||||
|
LockOpen as LockOpenIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogActions,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Divider,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
const AuthenticationModal = (props) => {
|
||||||
|
const {
|
||||||
|
globalUrl,
|
||||||
|
userdata,
|
||||||
|
|
||||||
|
selectedAppData,
|
||||||
|
getAppAuthentication,
|
||||||
|
appAuthentication,
|
||||||
|
setSelectedAction,
|
||||||
|
|
||||||
|
selectedMeta,
|
||||||
|
setSelectedMeta,
|
||||||
|
|
||||||
|
setAppAuthentication,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
|
||||||
|
const [selectedAuthentication, setSelectedAuthentication] = React.useState({});
|
||||||
|
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
|
||||||
|
const [authenticationType, setAuthenticationType] = React.useState({})
|
||||||
|
|
||||||
|
const [appid, setAppId] = useState("")
|
||||||
|
|
||||||
|
//const [appAuthentication, setAppAuthentication] = useState([]);
|
||||||
|
|
||||||
|
const { themeMode, supportEmail, brandColor } = useContext(Context)
|
||||||
|
const theme = getTheme(themeMode, brandColor)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
|
||||||
|
setAuthenticationType({
|
||||||
|
type: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedAppData.authentication = {
|
||||||
|
type: "",
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAuthenticationType(
|
||||||
|
selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? {
|
||||||
|
type: selectedAppData.authentication.type,
|
||||||
|
redirect_uri: selectedAppData.authentication.redirect_uri,
|
||||||
|
refresh_uri: selectedAppData.authentication.refresh_uri,
|
||||||
|
token_uri: selectedAppData.authentication.token_uri,
|
||||||
|
scope: selectedAppData.authentication.scope,
|
||||||
|
client_id: selectedAppData.authentication.client_id,
|
||||||
|
client_secret: selectedAppData.authentication.client_secret,
|
||||||
|
grant_type: selectedAppData.authentication.grant_type,
|
||||||
|
} : {
|
||||||
|
type: "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [selectedAppData])
|
||||||
|
|
||||||
|
if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) {
|
||||||
|
console.log("No app data for authentication modal");
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticationModalOpen === false) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
style={{
|
||||||
|
marginBottom: 20,
|
||||||
|
marginTop: 20,
|
||||||
|
flex: 1,
|
||||||
|
textTransform: "none",
|
||||||
|
textAlign: "left",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
color: "#2f2f2f",
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
minWidth: 275,
|
||||||
|
maxWidth: 275,
|
||||||
|
maxHeight: 50,
|
||||||
|
overflow: "hidden",
|
||||||
|
border: `1px solid ${theme.palette.inputColor}`,
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => {
|
||||||
|
setAppId(selectedAppData?.id)
|
||||||
|
setAuthenticationModalOpen(true)
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<span style={{display: "flex"}}>
|
||||||
|
<img
|
||||||
|
alt={selectedAppData?.name}
|
||||||
|
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||||
|
src={selectedAppData?.large_image}
|
||||||
|
/>
|
||||||
|
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||||
|
Authenticate {selectedAppData?.name?.replaceAll("_", " ", -1)}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 UpdateAppAuthentication = (data) => {
|
||||||
|
if (data === undefined || data === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
|
||||||
|
if (filteredData.length === 0) {
|
||||||
|
setAppAuthentication([]);
|
||||||
|
setSelectedAuthentication({});
|
||||||
|
} else {
|
||||||
|
setAppAuthentication(filteredData);
|
||||||
|
setSelectedAuthentication(filteredData[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const HandleAppAuthentication = () => {
|
||||||
|
|
||||||
|
const url = `${globalUrl}/api/v1/apps/authentication`;
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((responseJson) => {
|
||||||
|
if (responseJson.success === true) {
|
||||||
|
UpdateAppAuthentication(responseJson.data);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to get app authentication data");
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("error for app is :", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticationData = (props) => {
|
||||||
|
const selectedApp = props.app;
|
||||||
|
|
||||||
|
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||||||
|
app: JSON.parse(JSON.stringify(selectedApp)),
|
||||||
|
fields: {},
|
||||||
|
label: "",
|
||||||
|
usage: [
|
||||||
|
{
|
||||||
|
// workflow_id: workflow.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: uuidv4(),
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedApp.authentication === undefined ||
|
||||||
|
selectedApp.authentication.parameters === null ||
|
||||||
|
selectedApp.authentication.parameters === undefined ||
|
||||||
|
selectedApp.authentication.parameters.length === 0
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
|
||||||
|
<Typography variant="h4" id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||||
|
{selectedApp.name} does not require authentication
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationOption.app.actions = [];
|
||||||
|
|
||||||
|
for (let paramkey in selectedApp.authentication.parameters) {
|
||||||
|
if (
|
||||||
|
authenticationOption.fields[
|
||||||
|
selectedApp.authentication.parameters[paramkey].name
|
||||||
|
] === undefined
|
||||||
|
) {
|
||||||
|
authenticationOption.fields[
|
||||||
|
selectedApp.authentication.parameters[paramkey].name
|
||||||
|
] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNewAppAuth = (appAuthData, refresh) => {
|
||||||
|
setSelectedAuthentication(appAuthData);
|
||||||
|
var headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Org-Id"] = userdata?.active_org?.id
|
||||||
|
|
||||||
|
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(appAuthData),
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for setting app auth :O!");
|
||||||
|
|
||||||
|
if (response.status === 400) {
|
||||||
|
toast.error("Failed setting new auth. Please try again", {
|
||||||
|
"autoClose": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (!responseJson.success) {
|
||||||
|
toast.error("Error: " + responseJson.reason, {
|
||||||
|
"autoClose": false,
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
HandleAppAuthentication()
|
||||||
|
setAuthenticationModalOpen(false)
|
||||||
|
getAppAuthentication()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("New auth error: ", error.toString());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitCheck = () => {
|
||||||
|
if (authenticationOption.label.length === 0) {
|
||||||
|
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||||
|
}
|
||||||
|
for (let paramkey in selectedApp.authentication.parameters) {
|
||||||
|
if (
|
||||||
|
authenticationOption.fields[
|
||||||
|
selectedApp.authentication.parameters[paramkey].name
|
||||||
|
].length === 0
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
selectedApp.authentication.parameters[paramkey].value !== undefined &&
|
||||||
|
selectedApp.authentication.parameters[paramkey].value !== null &&
|
||||||
|
selectedApp.authentication.parameters[paramkey].value.length > 0
|
||||||
|
) {
|
||||||
|
authenticationOption.fields[
|
||||||
|
selectedApp.authentication.parameters[paramkey].name
|
||||||
|
] = selectedApp.authentication.parameters[paramkey].value;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
selectedApp.authentication.parameters[paramkey].schema.type === "bool"
|
||||||
|
) {
|
||||||
|
authenticationOption.fields[
|
||||||
|
selectedApp.authentication.parameters[paramkey].name
|
||||||
|
] = "false";
|
||||||
|
} else {
|
||||||
|
toast(
|
||||||
|
"Field " +
|
||||||
|
selectedApp.authentication.parameters[paramkey].name +
|
||||||
|
" can't be empty"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||||
|
var newFields = [];
|
||||||
|
for (let authkey in newAuthOption.fields) {
|
||||||
|
const value = newAuthOption.fields[authkey];
|
||||||
|
newFields.push({
|
||||||
|
"key": authkey,
|
||||||
|
"value": value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newAuthOption.fields = newFields
|
||||||
|
setNewAppAuth(newAuthOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||||||
|
authenticationOption.label = selectedApp.name + " authentication";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DialogTitle id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||||
|
<div style={{ color: theme.palette.textColor }}>
|
||||||
|
Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://shuffler.io/docs/apps#authentication"
|
||||||
|
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||||
|
>
|
||||||
|
What is app authentication?
|
||||||
|
</a>
|
||||||
|
<div />
|
||||||
|
These are required fields for authenticating with {selectedApp.name}
|
||||||
|
<div style={{ marginTop: 15 }} />
|
||||||
|
<b>Label for you to remember</b>
|
||||||
|
<TextField
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
style: {
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
color="primary"
|
||||||
|
placeholder={"Auth july 2020"}
|
||||||
|
defaultValue={`Auth for ${selectedApp.name}`}
|
||||||
|
onChange={(event) => {
|
||||||
|
authenticationOption.label = event.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
marginTop: 15,
|
||||||
|
marginBottom: 15,
|
||||||
|
backgroundColor: "rgb(91, 96, 100)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div />
|
||||||
|
{selectedApp.authentication.parameters.map((data, index) => {
|
||||||
|
if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} style={{ marginTop: 10 }}>
|
||||||
|
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||||
|
<b>{data.name}</b>
|
||||||
|
|
||||||
|
{data.schema !== undefined &&
|
||||||
|
data.schema !== null &&
|
||||||
|
data.schema.type === "bool" ? (
|
||||||
|
<Select
|
||||||
|
MenuProps={{
|
||||||
|
disableScrollLock: true,
|
||||||
|
}}
|
||||||
|
SelectDisplayProps={{
|
||||||
|
style: {
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
defaultValue={"false"}
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
authenticationOption.fields[data.name] = e.target.value;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.surfaceColor,
|
||||||
|
color: theme.palette.textColor,
|
||||||
|
height: 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
key={"false"}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
color: theme.palette.textColor,
|
||||||
|
}}
|
||||||
|
value={"false"}
|
||||||
|
>
|
||||||
|
false
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
key={"true"}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
color: theme.palette.textColor,
|
||||||
|
}}
|
||||||
|
value={"true"}
|
||||||
|
>
|
||||||
|
true
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
style: {
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
type={
|
||||||
|
data.example !== undefined && data.example.includes("***")
|
||||||
|
? "password"
|
||||||
|
: "text"
|
||||||
|
}
|
||||||
|
color="primary"
|
||||||
|
defaultValue={
|
||||||
|
data.value !== undefined && data.value !== null
|
||||||
|
? data.value
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
placeholder={data.example}
|
||||||
|
onChange={(event) => {
|
||||||
|
authenticationOption.fields[data.name] =
|
||||||
|
event.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
style={{}}
|
||||||
|
onClick={() => {
|
||||||
|
setAuthenticationModalOpen(false);
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{}}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setAuthenticationOptions(authenticationOption);
|
||||||
|
handleSubmitCheck()
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticationModal = authenticationModalOpen ? (
|
||||||
|
<Dialog
|
||||||
|
PaperComponent={PaperComponent}
|
||||||
|
aria-labelledby="draggable-dialog-title"
|
||||||
|
hideBackdrop={true}
|
||||||
|
disableEnforceFocus={true}
|
||||||
|
disableBackdropClick={true}
|
||||||
|
style={{ pointerEvents: "none" }}
|
||||||
|
open={authenticationModalOpen}
|
||||||
|
onClose={() => {setSelectedMeta(undefined)}}
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
pointerEvents: "auto",
|
||||||
|
color: theme.palette.textColor,
|
||||||
|
minWidth: 1100,
|
||||||
|
minHeight: 800,
|
||||||
|
maxHeight: 800,
|
||||||
|
padding: 15,
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: 10012,
|
||||||
|
border: theme.palette.defaultBorder,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
zIndex: 5000,
|
||||||
|
position: "absolute",
|
||||||
|
top: 20,
|
||||||
|
right: 75,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ selectedAppData.reference_info === undefined ||
|
||||||
|
selectedAppData.reference_info === null ||
|
||||||
|
selectedAppData.reference_info.github_url === undefined ||
|
||||||
|
selectedAppData.reference_info.github_url === null ||
|
||||||
|
selectedAppData.reference_info.github_url.length === 0 ? (
|
||||||
|
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={"https://github.com/shuffle/python-apps"}
|
||||||
|
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={`Documentation image for ${selectedAppData.name}`}
|
||||||
|
src={selectedAppData.large_image}
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
border: "2px solid rgba(255,255,255,0.6)",
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
maxHeight: 30,
|
||||||
|
maxWidth: 30,
|
||||||
|
overflow: "hidden",
|
||||||
|
fontSize: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={selectedAppData.reference_info.github_url}
|
||||||
|
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={`Documentation image for ${selectedAppData.name}`}
|
||||||
|
src={selectedAppData.large_image}
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
border: "2px solid rgba(255,255,255,0.6)",
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
maxWidth: 30,
|
||||||
|
maxHeight: 30,
|
||||||
|
overflow: "hidden",
|
||||||
|
fontSize: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
color="primary"
|
||||||
|
title={`Move window`}
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
id="draggable-dialog-title"
|
||||||
|
style={{
|
||||||
|
zIndex: 5000,
|
||||||
|
position: "absolute",
|
||||||
|
top: 14,
|
||||||
|
right: 50,
|
||||||
|
color: "grey",
|
||||||
|
|
||||||
|
cursor: "move",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragIndicatorIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton
|
||||||
|
style={{
|
||||||
|
zIndex: 5000,
|
||||||
|
position: "absolute",
|
||||||
|
top: 14,
|
||||||
|
right: 18,
|
||||||
|
color: "grey",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setAuthenticationModalOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 2,
|
||||||
|
padding: 0,
|
||||||
|
minHeight: isMobile ? "90%" : 800,
|
||||||
|
maxHeight: isMobile ? "90%" : 800,
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: isMobile ? "auto" : "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{authenticationType?.type === "oauth2" || authenticationType?.type === "oauth2-app" ?
|
||||||
|
<AuthenticationOauth2
|
||||||
|
selectedApp={selectedAppData}
|
||||||
|
selectedAction={{
|
||||||
|
"app_name": selectedAppData.name,
|
||||||
|
"app_id": selectedAppData.id,
|
||||||
|
"app_version": selectedAppData.version,
|
||||||
|
"large_image": selectedAppData.large_image,
|
||||||
|
}}
|
||||||
|
authenticationType={authenticationType}
|
||||||
|
getAppAuthentication={getAppAuthentication}
|
||||||
|
appAuthentication={appAuthentication}
|
||||||
|
setAuthenticationModalOpen={setAuthenticationModalOpen}
|
||||||
|
isCloud={isCloud}
|
||||||
|
org_id={userdata?.active_org?.id}
|
||||||
|
setSelectedAction={setSelectedAction}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<AuthenticationData app={selectedAppData} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 3,
|
||||||
|
borderLeft: `1px solid ${theme.palette.inputColor}`,
|
||||||
|
padding: "70px 30px 30px 30px",
|
||||||
|
maxHeight: 630,
|
||||||
|
minHeight: 630,
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedAppData?.documentation === undefined ||
|
||||||
|
selectedAppData?.documentation === null ||
|
||||||
|
selectedAppData?.documentation.length === 0 ? (
|
||||||
|
<span
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
<div style={{textAlign: "left", }}>
|
||||||
|
<Markdown
|
||||||
|
components={{
|
||||||
|
img: Img,
|
||||||
|
code: CodeHandler,
|
||||||
|
h1: Heading,
|
||||||
|
h2: Heading,
|
||||||
|
h3: Heading,
|
||||||
|
h4: Heading,
|
||||||
|
h5: Heading,
|
||||||
|
h6: Heading,
|
||||||
|
a: OuterLink,
|
||||||
|
}}
|
||||||
|
id="markdown_wrapper"
|
||||||
|
escapeHtml={false}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
minWidth: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedAppData?.description}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
|
<Divider
|
||||||
|
style={{
|
||||||
|
marginTop: 25,
|
||||||
|
marginBottom: 25,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.6)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
marginBottom: 30,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" style={{marginBottom: 25, }}>
|
||||||
|
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
toast.success("Opening remote Github documentation link. Thanks for contributing!")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedAppData.name.toLowerCase()}.md`, "_blank")
|
||||||
|
}, 2500)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon /> Create Docs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||||
|
Want to help the making of, or improve this app?{" "}
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://discord.gg/B2CBzUm"
|
||||||
|
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||||
|
>
|
||||||
|
Join the community on Discord!
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" style={{ marginTop: 50 }}>
|
||||||
|
Want to help change this app directly?
|
||||||
|
</Typography>
|
||||||
|
{selectedAppData.reference_info === undefined ||
|
||||||
|
selectedAppData.reference_info === null ||
|
||||||
|
selectedAppData.reference_info.github_url === undefined ||
|
||||||
|
selectedAppData.reference_info.github_url === null ||
|
||||||
|
selectedAppData.reference_info.github_url.length === 0 ? (
|
||||||
|
<span>
|
||||||
|
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={"https://github.com/shuffle/python-apps"}
|
||||||
|
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||||
|
>
|
||||||
|
Check it out on Github!
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={selectedAppData.reference_info.github_url}
|
||||||
|
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||||
|
>
|
||||||
|
Check it out on Github!
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ?
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
marginBottom: 30,
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 3, display: "flex", vAlign: "center", position: "sticky", top: 50, }}>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography style={{ display: "inline", marginTop: 6 }}>
|
||||||
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={selectedMeta.link}
|
||||||
|
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||||
|
>
|
||||||
|
<Button style={{ color: theme.palette.textColor, }} variant="outlined" color="secondary">
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{isMobile ? null : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: theme.palette.textColor,
|
||||||
|
marginLeft: 50,
|
||||||
|
marginRight: 50,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography style={{ display: "inline", marginTop: 11 }}>
|
||||||
|
{selectedMeta.read_time} minute
|
||||||
|
{selectedMeta.read_time === 1 ? "" : "s"} to read
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 2 }}>
|
||||||
|
{isMobile ||
|
||||||
|
selectedMeta.contributors === undefined ||
|
||||||
|
selectedMeta.contributors === null ? (
|
||||||
|
""
|
||||||
|
) : (
|
||||||
|
<div style={{ margin: 10, height: "100%", display: "inline" }}>
|
||||||
|
{selectedMeta.contributors.slice(0, 7).map((data, index) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={data.url}
|
||||||
|
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||||
|
>
|
||||||
|
<Tooltip title={data.url} placement="bottom">
|
||||||
|
<img
|
||||||
|
alt={data.url}
|
||||||
|
src={data.image}
|
||||||
|
style={{
|
||||||
|
marginTop: 5,
|
||||||
|
marginRight: 10,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 40,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
|
||||||
|
<Markdown
|
||||||
|
components={{
|
||||||
|
img: Img,
|
||||||
|
code: CodeHandler,
|
||||||
|
h1: Heading,
|
||||||
|
h2: Heading,
|
||||||
|
h3: Heading,
|
||||||
|
h4: Heading,
|
||||||
|
h5: Heading,
|
||||||
|
h6: Heading,
|
||||||
|
a: OuterLink,
|
||||||
|
}}
|
||||||
|
id="markdown_wrapper"
|
||||||
|
escapeHtml={false}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%", minWidth: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedAppData.documentation}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return authenticationModal
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthenticationModal;
|
||||||
@@ -47,7 +47,9 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
Padding,
|
Padding,
|
||||||
Edit,
|
Edit,
|
||||||
Search as SearchIcon
|
Search as SearchIcon,
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
//import { useAlert
|
//import { useAlert
|
||||||
@@ -61,6 +63,62 @@ import { Context } from "../context/ContextApi.jsx";
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { DataGrid } from "@mui/x-data-grid";
|
import { DataGrid } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => {
|
||||||
|
var isProdStatusOn;
|
||||||
|
if (selectedOrganization !== undefined && selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions[0] !== undefined) {
|
||||||
|
isProdStatusOn = selectedOrganization?.subscriptions[0]?.name?.toLowerCase()?.includes("enterprise") && selectedOrganization?.subscriptions[0]?.active;
|
||||||
|
} else {
|
||||||
|
isProdStatusOn = false;
|
||||||
|
}
|
||||||
|
const rows = [
|
||||||
|
{ label: 'Licensed', ok: isProdStatusOn },
|
||||||
|
{ label: 'Multi-Tenant', ok: isProdStatusOn },
|
||||||
|
{ label: 'High Availability', ok: isProdStatusOn },
|
||||||
|
{ label: 'Robust Infrastructure', ok: isProdStatusOn },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', maxWidth: 800, padding: '0px 24px 24px 0px', height: 445 , display: 'flex', flexDirection: 'column', alignItems: 'flex-start'}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 8 }}>
|
||||||
|
<Typography variant="h5" style={{ fontWeight: 600, fontFamily: theme.typography.fontFamily }}>Production Status</Typography>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 14px', borderRadius: 16, background: isProdStatusOn ? 'rgba(43,192,126,0.1)' : 'rgba(253,76,98,0.1)' }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: 999, background: isProdStatusOn ? '#2BC07E' : '#FD4C62' }} />
|
||||||
|
<Typography variant="caption" style={{ color: isProdStatusOn ? '#2BC07E' : '#FD4C62', fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{isProdStatusOn ? "ON" : "OFF"}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 18, fontFamily: theme.typography.fontFamily }}>
|
||||||
|
Monitor your production status to stay informed about available features.
|
||||||
|
</Typography>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 12, fontFamily: theme.typography.fontFamily }}>
|
||||||
|
{row.ok ? (
|
||||||
|
<CheckCircleIcon style={{ color: '#2BC07E' }} />
|
||||||
|
) : (
|
||||||
|
<CancelIcon style={{ color: '#FD4C62' }} />
|
||||||
|
)}
|
||||||
|
<Typography variant="body1" style={{ fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{row.label}</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{
|
||||||
|
width: '100%',
|
||||||
|
marginTop: 32,
|
||||||
|
marginBottom: 16,
|
||||||
|
borderColor: theme.palette.defaultBorder,
|
||||||
|
|
||||||
|
}} />
|
||||||
|
<Typography variant="body1" color="textPrimary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
|
||||||
|
Shuffle Enterprise is designed for organizations that require scalability, high availability, dedicated support and more to run mission-critical workflows in production environments.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="textSecondary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
|
||||||
|
More about upgrading below. If you want to know more, please contact <a href="mailto:support@shuffler.io?subject=Tell%20me%20about%20Production%20readiness" style={{ color: '#f85a3e', textDecoration: 'none' }}>support@shuffler.io</a> directly.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Billing = memo((props) => {
|
const Billing = memo((props) => {
|
||||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
||||||
//const alert = useAlert();
|
//const alert = useAlert();
|
||||||
@@ -87,6 +145,8 @@ const Billing = memo((props) => {
|
|||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1);
|
const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1);
|
||||||
const [deleteAlertVerification, setDeleteAlertVerification] = useState(false);
|
const [deleteAlertVerification, setDeleteAlertVerification] = useState(false);
|
||||||
|
const [supportAppRunLimit, setSupportAppRunLimit] = useState(selectedOrganization?.billing?.internal_app_runs_hard_limit || '');
|
||||||
|
const [supportLimitDialogOpen, setSupportLimitDialogOpen] = useState(false);
|
||||||
const [isScale, setIsScale] = useState(false);
|
const [isScale, setIsScale] = useState(false);
|
||||||
const [currentTab, setCurrentTab] = useState(0)
|
const [currentTab, setCurrentTab] = useState(0)
|
||||||
const [allChildOrgs, setAllChildOrgs] = useState([])
|
const [allChildOrgs, setAllChildOrgs] = useState([])
|
||||||
@@ -145,6 +205,9 @@ const Billing = memo((props) => {
|
|||||||
|
|
||||||
const findCurrentIndex = sortedAlertThresholds.some(threshold => threshold.Email_send === false);
|
const findCurrentIndex = sortedAlertThresholds.some(threshold => threshold.Email_send === false);
|
||||||
setCurrentIndex(findCurrentIndex ? sortedAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
|
setCurrentIndex(findCurrentIndex ? sortedAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
|
||||||
|
if (selectedOrganization?.Billing?.internal_app_runs_hard_limit !== undefined && selectedOrganization?.Billing?.internal_app_runs_hard_limit !== null && selectedOrganization?.Billing?.internal_app_runs_hard_limit > 0) {
|
||||||
|
setSupportAppRunLimit(selectedOrganization?.Billing?.internal_app_runs_hard_limit)
|
||||||
|
}
|
||||||
|
|
||||||
}, [selectedOrganization]);
|
}, [selectedOrganization]);
|
||||||
|
|
||||||
@@ -1903,6 +1966,52 @@ const Billing = memo((props) => {
|
|||||||
setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]);
|
setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleUpdateSupportAppRunLimit = () => {
|
||||||
|
if (!supportAppRunLimit || isNaN(supportAppRunLimit) || supportAppRunLimit < 0) {
|
||||||
|
toast.error("Please enter a valid app run limit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast("Updating app run limit. Please wait...");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
org_id: selectedOrganization.id,
|
||||||
|
editing: "internal_appruns_hard_limit",
|
||||||
|
billing: {
|
||||||
|
internal_app_runs_hard_limit: parseInt(supportAppRunLimit) || 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = globalUrl + "/api/v1/orgs/" + selectedOrganization.id;
|
||||||
|
fetch(url, {
|
||||||
|
mode: "cors",
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: "include",
|
||||||
|
crossDomain: true,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
toast.success("Successfully updated app run limit");
|
||||||
|
setSupportLimitDialogOpen(false);
|
||||||
|
if (handleGetOrg !== undefined) {
|
||||||
|
handleGetOrg(selectedOrganization.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to update app run limit. Please try again.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Error updating app run limit:", error);
|
||||||
|
toast.error("Failed to update app run limit. Please try again.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updateAlertThreshold = (index, field, value) => {
|
const updateAlertThreshold = (index, field, value) => {
|
||||||
|
|
||||||
const totalValue = userdata.app_execution_limit;
|
const totalValue = userdata.app_execution_limit;
|
||||||
@@ -2025,29 +2134,41 @@ const Billing = memo((props) => {
|
|||||||
}
|
}
|
||||||
}, [isChildOrg, currentTab]);
|
}, [isChildOrg, currentTab]);
|
||||||
|
|
||||||
|
// Update supportAppRunLimit when selectedOrganization changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedOrganization?.billing?.internal_app_runs_hard_limit !== undefined) {
|
||||||
|
setSupportAppRunLimit(selectedOrganization.billing.internal_app_runs_hard_limit);
|
||||||
|
}
|
||||||
|
}, [selectedOrganization?.billing?.internal_app_runs_hard_limit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
|
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||||
<div style={{ height: "100%", width: "100%"}}>
|
<div style={{ width: "100%", padding: 24, }}>
|
||||||
<div style={{ width: "100%",}}>
|
<div style={{ width: "100%", maxWidth: 800, }}>
|
||||||
|
|
||||||
|
{isCloud ? null : <ProductionStatus selectedOrganization={selectedOrganization} userdata={userdata} isCloud={isCloud} theme={theme} />}
|
||||||
|
|
||||||
{addDealModal}
|
{addDealModal}
|
||||||
{clickedFromOrgTab ?
|
{clickedFromOrgTab ?
|
||||||
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 0, }}>Billing & Licensing</Typography> :
|
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 24, }}>Billing & Licensing</Typography>
|
||||||
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
:
|
||||||
Billing & Licensing
|
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||||
</Typography>}
|
Billing & Licensing
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
|
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
|
||||||
<>
|
<>
|
||||||
{clickedFromOrgTab ?
|
{clickedFromOrgTab ?
|
||||||
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16 }}>{isCloud ?
|
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16 }}>{isCloud ?
|
||||||
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
||||||
:
|
:
|
||||||
"Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update."
|
!(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required at scale. We offer a license with HA guarantees, higher limits, along along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information."
|
||||||
}</Typography> :
|
}</Typography> :
|
||||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
|
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
|
||||||
{isCloud ?
|
{isCloud ?
|
||||||
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
|
||||||
:
|
:
|
||||||
"Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update."
|
!(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required at scale. We offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information."
|
||||||
}
|
}
|
||||||
</Typography>}
|
</Typography>}
|
||||||
</> }
|
</> }
|
||||||
@@ -2118,6 +2239,8 @@ const Billing = memo((props) => {
|
|||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
selectedOrganization={selectedOrganization}
|
selectedOrganization={selectedOrganization}
|
||||||
billingInfo={billingInfo}
|
billingInfo={billingInfo}
|
||||||
|
monthlyAppRunsParent={monthlyAppRunsParent}
|
||||||
|
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
userdata={userdata}
|
userdata={userdata}
|
||||||
stripeKey={stripeKey}
|
stripeKey={stripeKey}
|
||||||
@@ -2154,6 +2277,8 @@ const Billing = memo((props) => {
|
|||||||
isLoggedIn={isLoggedIn}
|
isLoggedIn={isLoggedIn}
|
||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
selectedOrganization={selectedOrganization}
|
selectedOrganization={selectedOrganization}
|
||||||
|
monthlyAppRunsParent={monthlyAppRunsParent}
|
||||||
|
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
|
||||||
billingInfo={billingInfo}
|
billingInfo={billingInfo}
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
userdata={userdata}
|
userdata={userdata}
|
||||||
@@ -2639,6 +2764,122 @@ const Billing = memo((props) => {
|
|||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{userdata.support === true && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 50,
|
||||||
|
padding: 20,
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 193, 7, 0.3)' : 'rgba(255, 193, 7, 0.4)'}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 193, 7, 0.02)' : 'rgba(255, 193, 7, 0.03)'
|
||||||
|
}}>
|
||||||
|
<Typography color="textPrimary" style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 10
|
||||||
|
}}>
|
||||||
|
⚠️ Support Only - App Run Limit Control
|
||||||
|
</Typography>
|
||||||
|
<Typography color="textSecondary" style={{ fontSize: 16, marginBottom: 15 }}>
|
||||||
|
<strong>Note:</strong> Setting an app run hard limit below current usage will immediately stop all workflow executions for this organization.
|
||||||
|
</Typography>
|
||||||
|
<Typography color="textSecondary" style={{ fontSize: 14, marginBottom: 20 }}>
|
||||||
|
Current app runs this month: <strong>{Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}</strong> / <strong>{userdata.app_execution_limit}</strong>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: '500',
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 193, 7, 0.5)' : 'rgba(255, 152, 0, 0.6)',
|
||||||
|
color: theme.palette.mode === 'dark' ? '#fff' : 'rgba(255, 152, 0, 0.9)',
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.palette.mode === 'dark' ? '#fff' : 'rgba(255, 152, 0, 1)',
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 193, 7, 0.08)' : 'rgba(255, 152, 0, 0.04)',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => setSupportLimitDialogOpen(true)}
|
||||||
|
>
|
||||||
|
Set App Run Limit
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Support Limit Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={supportLimitDialogOpen}
|
||||||
|
onClose={() => setSupportLimitDialogOpen(false)}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||||
|
border: theme?.palette?.DialogStyle?.border,
|
||||||
|
minWidth: '440px',
|
||||||
|
fontFamily: theme?.typography?.fontFamily,
|
||||||
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||||
|
zIndex: 1000,
|
||||||
|
'& .MuiDialogContent-root': {
|
||||||
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||||
|
},
|
||||||
|
'& .MuiDialogTitle-root': {
|
||||||
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||||
|
},
|
||||||
|
'& .MuiDialogActions-root': {
|
||||||
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle color={theme.palette.text.primary}>
|
||||||
|
⚠️ Set App Run Limit
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText style={{ marginBottom: 20 }}>
|
||||||
|
<strong>Note:</strong> This will set a hard limit on app executions. If the organization reaches this limit, all workflow executions will be stopped.
|
||||||
|
</DialogContentText>
|
||||||
|
<DialogContentText style={{ marginBottom: 20 }}>
|
||||||
|
Current usage: <strong>{Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}</strong> app runs this month
|
||||||
|
</DialogContentText>
|
||||||
|
<DialogContentText style={{ marginBottom: 20 }}>
|
||||||
|
Current hard limit: <strong>{selectedOrganization?.billing?.internal_app_runs_hard_limit || 'Not set'}</strong>
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="App Run Limit"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={supportAppRunLimit}
|
||||||
|
onChange={(e) => setSupportAppRunLimit(e.target.value)}
|
||||||
|
inputProps={{ min: 0 }}
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
helperText="Set to 0 to completely disable app run hard limit"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => setSupportLimitDialogOpen(false)}
|
||||||
|
style={{ textTransform: 'none' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdateSupportAppRunLimit}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
style={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: '500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply Limit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
): null}
|
): null}
|
||||||
</div>
|
</div>
|
||||||
@@ -2746,6 +2987,7 @@ const Billing = memo((props) => {
|
|||||||
userdata={userdata}
|
userdata={userdata}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
syncStats={true}
|
syncStats={true}
|
||||||
|
statistics={statistics}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -3275,7 +3517,7 @@ const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maxHeight: "1700px",
|
maxHeight: 3000,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
||||||
scrollbarWidth: 'thin'
|
scrollbarWidth: 'thin'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useContext, memo, useMemo } from 'react';
|
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
||||||
|
|
||||||
import {getTheme} from '../theme.jsx';
|
import {getTheme} from '../theme.jsx';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@@ -73,6 +73,7 @@ const AppStats = (defaultprops) => {
|
|||||||
const [resultRows, setResultRows] = useState([])
|
const [resultRows, setResultRows] = useState([])
|
||||||
const [resultLoading, setResultLoading] = useState(true)
|
const [resultLoading, setResultLoading] = useState(true)
|
||||||
const { themeMode, brandColor } = useContext(Context);
|
const { themeMode, brandColor } = useContext(Context);
|
||||||
|
const [onpremAppRuns, setOnpremAppRuns] = useState(0)
|
||||||
const theme = getTheme(themeMode, brandColor)
|
const theme = getTheme(themeMode, brandColor)
|
||||||
|
|
||||||
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
|
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
|
||||||
@@ -83,12 +84,156 @@ const AppStats = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleDataSetting = useCallback((inputdata, grouping) => {
|
||||||
|
if (inputdata === undefined || inputdata === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
||||||
|
const dailyStats = inputdata[statKey]
|
||||||
|
if (dailyStats === undefined || dailyStats === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var appRuns = {
|
||||||
|
"key": "App Runs",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
|
||||||
|
var childorgappRuns = {
|
||||||
|
"key": "Child Org App Runs",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
|
||||||
|
var workflowRuns = {
|
||||||
|
"key": "Workflow Runs (includes subflows)",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
|
||||||
|
var subflowRuns = {
|
||||||
|
"key": "Subflow Runs",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
|
||||||
|
var appcostRuns = {
|
||||||
|
"key": "Cost of App Runs",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in dailyStats) {
|
||||||
|
// Always skips first one as it has accumulated data in it
|
||||||
|
if (key === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = dailyStats[key]
|
||||||
|
if (item["date"] === undefined) {
|
||||||
|
console.log("No date: ", item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if app_executions key in item
|
||||||
|
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
|
||||||
|
appRuns["data"].push({
|
||||||
|
key: new Date(item["date"]).toISOString(),
|
||||||
|
data: item["app_executions"]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add number
|
||||||
|
appcostRuns["data"].push({
|
||||||
|
key: new Date(item["date"]).toISOString(),
|
||||||
|
data: (item["app_executions"] * invocationCost).toFixed(2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
|
||||||
|
childorgappRuns["data"].push({
|
||||||
|
key: new Date(item["date"]).toISOString(),
|
||||||
|
data: item["child_app_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if workflow_executions key in item
|
||||||
|
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
|
||||||
|
workflowRuns["data"].push({
|
||||||
|
key: new Date(item["date"]).toISOString(),
|
||||||
|
data: item["workflow_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
|
||||||
|
subflowRuns["data"].push({
|
||||||
|
key: new Date(item["date"]).toISOString(),
|
||||||
|
data: item["subflow_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add today's data if endTime is not set or if today falls within the selected date range
|
||||||
|
const today = new Date()
|
||||||
|
const todayStartOfDay = new Date(today)
|
||||||
|
todayStartOfDay.setHours(0, 0, 0, 0)
|
||||||
|
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
|
||||||
|
(new Date(endTime) >= todayStartOfDay)
|
||||||
|
|
||||||
|
if (!syncStats && shouldAddTodayData) {
|
||||||
|
// Adds data for today
|
||||||
|
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
|
||||||
|
appRuns["data"].push({
|
||||||
|
key: new Date().toISOString(),
|
||||||
|
data: inputdata["daily_app_executions"]
|
||||||
|
})
|
||||||
|
|
||||||
|
appcostRuns["data"].push({
|
||||||
|
key: new Date().toISOString(),
|
||||||
|
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
|
||||||
|
childorgappRuns["data"].push({
|
||||||
|
key: new Date().toISOString(),
|
||||||
|
data: inputdata["daily_child_app_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
|
||||||
|
workflowRuns["data"].push({
|
||||||
|
key: new Date().toISOString(),
|
||||||
|
data: inputdata["daily_workflow_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
|
||||||
|
subflowRuns["data"].push({
|
||||||
|
key: new Date().toISOString(),
|
||||||
|
data: inputdata["daily_subflow_executions"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only for parent orgs
|
||||||
|
if (childorgappRuns["data"].length > 0) {
|
||||||
|
setChildOrgsAppRuns(childorgappRuns)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubflowRuns(subflowRuns)
|
||||||
|
setWorkflowRuns(workflowRuns)
|
||||||
|
setAppruns(appRuns)
|
||||||
|
setApprunCosts(appcostRuns)
|
||||||
|
}, [syncStats, endTime, startTime])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statistics && statistics?.org_id?.length > 0) {
|
if (statistics && statistics?.org_id?.length > 0) {
|
||||||
handleDataSetting(statistics, "day")
|
handleDataSetting(statistics, "day")
|
||||||
}
|
}
|
||||||
}, [statistics])
|
}, [statistics])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStartTime("")
|
||||||
|
setEndTime("")
|
||||||
|
}, [currentTab])
|
||||||
|
|
||||||
const getWorkflowStats = async (workflow, startTime, endTime) => {
|
const getWorkflowStats = async (workflow, startTime, endTime) => {
|
||||||
|
|
||||||
if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
|
if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
|
||||||
@@ -227,7 +372,7 @@ const AppStats = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
||||||
if (statistics[statKey] === undefined || statistics[statKey] === null) {
|
if (!syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) {
|
||||||
setFilteredStatistics(statistics)
|
setFilteredStatistics(statistics)
|
||||||
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
|
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
|
||||||
return
|
return
|
||||||
@@ -356,17 +501,55 @@ const AppStats = (defaultprops) => {
|
|||||||
workflowexecutions += item["workflow_executions"]
|
workflowexecutions += item["workflow_executions"]
|
||||||
appexecutions += item["app_executions"]
|
appexecutions += item["app_executions"]
|
||||||
|
|
||||||
if (currentTab === 0) {
|
if (currentTab === 0 || currentTab === 3) {
|
||||||
appexecutions += (item["child_app_executions"] ?? 0)
|
appexecutions += (item["child_app_executions"] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
estimatedcost += (item["app_executions"] * invocationCost)
|
estimatedcost += (item["app_executions"] * invocationCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const isCurrentMonthSelected =
|
||||||
|
(startTime === "" && endTime === "") ||
|
||||||
|
(
|
||||||
|
new Date(foundstarttime).getMonth() === today.getMonth() &&
|
||||||
|
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
|
||||||
|
new Date(foundendtime).getMonth() === today.getMonth() &&
|
||||||
|
new Date(foundendtime).getFullYear() === today.getFullYear()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!syncStats && isCurrentMonthSelected) {
|
||||||
|
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
|
||||||
|
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tmpstats["monthly_workflow_executions"] = workflowexecutions
|
tmpstats["monthly_workflow_executions"] = workflowexecutions
|
||||||
tmpstats["monthly_app_executions"] = appexecutions
|
tmpstats["monthly_app_executions"] = appexecutions
|
||||||
|
if (syncStats) {
|
||||||
|
setOnpremAppRuns(appexecutions)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const today = new Date();
|
||||||
|
const isCurrentMonthSelected =
|
||||||
|
(startTime === "" && endTime === "") ||
|
||||||
|
(
|
||||||
|
new Date(foundstarttime).getMonth() === today.getMonth() &&
|
||||||
|
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
|
||||||
|
new Date(foundendtime).getMonth() === today.getMonth() &&
|
||||||
|
new Date(foundendtime).getFullYear() === today.getFullYear()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!syncStats && isCurrentMonthSelected) {
|
||||||
|
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
|
||||||
|
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpstats["monthly_app_executions"] = appexecutions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Make estimatedcost have max 2 decimals
|
// Make estimatedcost have max 2 decimals
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
// Exclude includedExecutions*month
|
// Exclude includedExecutions*month
|
||||||
@@ -380,11 +563,11 @@ const AppStats = (defaultprops) => {
|
|||||||
handleDataSetting(tmpstats, "day")
|
handleDataSetting(tmpstats, "day")
|
||||||
// if we have done monthly reset than only show monthly app runs as current month app run
|
// if we have done monthly reset than only show monthly app runs as current month app run
|
||||||
const currentMonth = new Date().getMonth() + 1
|
const currentMonth = new Date().getMonth() + 1
|
||||||
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
|
if (!syncStats && !monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
|
||||||
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
|
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
|
if (!syncStats && !monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
|
||||||
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
|
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +580,7 @@ const AppStats = (defaultprops) => {
|
|||||||
loadWorkflowStats(foundWorkflows, startTime, endTime)
|
loadWorkflowStats(foundWorkflows, startTime, endTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [statistics, startTime, endTime])
|
}, [statistics, startTime, endTime, syncStats, currentTab, handleDataSetting])
|
||||||
|
|
||||||
const handleStartTimeChange = (date) => {
|
const handleStartTimeChange = (date) => {
|
||||||
setStartTime(date)
|
setStartTime(date)
|
||||||
@@ -406,143 +589,6 @@ const AppStats = (defaultprops) => {
|
|||||||
const handleEndTimeChange = (date) => {
|
const handleEndTimeChange = (date) => {
|
||||||
setEndTime(date)
|
setEndTime(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDataSetting = (inputdata, grouping) => {
|
|
||||||
if (inputdata === undefined || inputdata === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
|
||||||
const dailyStats = inputdata[statKey]
|
|
||||||
if (dailyStats === undefined || dailyStats === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var appRuns = {
|
|
||||||
"key": "App Runs",
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
|
|
||||||
var childorgappRuns = {
|
|
||||||
"key": "Child Org App Runs",
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
|
|
||||||
var workflowRuns = {
|
|
||||||
"key": "Workflow Runs (includes subflows)",
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
|
|
||||||
var subflowRuns = {
|
|
||||||
"key": "Subflow Runs",
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
|
|
||||||
var appcostRuns = {
|
|
||||||
"key": "Cost of App Runs",
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let key in dailyStats) {
|
|
||||||
// Always skips first one as it has accumulated data in it
|
|
||||||
if (key === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = dailyStats[key]
|
|
||||||
if (item["date"] === undefined) {
|
|
||||||
console.log("No date: ", item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if app_executions key in item
|
|
||||||
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
|
|
||||||
appRuns["data"].push({
|
|
||||||
key: new Date(item["date"]),
|
|
||||||
data: item["app_executions"]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add number
|
|
||||||
appcostRuns["data"].push({
|
|
||||||
key: new Date(item["date"]),
|
|
||||||
data: (item["app_executions"] * invocationCost).toFixed(2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
|
|
||||||
childorgappRuns["data"].push({
|
|
||||||
key: new Date(item["date"]),
|
|
||||||
data: item["child_app_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if workflow_executions key in item
|
|
||||||
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
|
|
||||||
workflowRuns["data"].push({
|
|
||||||
key: new Date(item["date"]),
|
|
||||||
data: item["workflow_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
|
|
||||||
subflowRuns["data"].push({
|
|
||||||
key: new Date(item["date"]),
|
|
||||||
data: item["subflow_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add today's data if endTime is not set or if today falls within the selected date range
|
|
||||||
const today = new Date()
|
|
||||||
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
|
|
||||||
(new Date(endTime) >= today.setHours(0, 0, 0, 0))
|
|
||||||
|
|
||||||
if (shouldAddTodayData) {
|
|
||||||
// Adds data for today
|
|
||||||
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
|
|
||||||
appRuns["data"].push({
|
|
||||||
key: new Date(),
|
|
||||||
data: inputdata["daily_app_executions"]
|
|
||||||
})
|
|
||||||
|
|
||||||
appcostRuns["data"].push({
|
|
||||||
key: new Date(),
|
|
||||||
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
|
|
||||||
childorgappRuns["data"].push({
|
|
||||||
key: new Date(),
|
|
||||||
data: inputdata["daily_child_app_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
|
|
||||||
workflowRuns["data"].push({
|
|
||||||
key: new Date(),
|
|
||||||
data: inputdata["daily_workflow_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
|
|
||||||
subflowRuns["data"].push({
|
|
||||||
key: new Date(),
|
|
||||||
data: inputdata["daily_subflow_executions"]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only for parent orgs
|
|
||||||
if (childorgappRuns["data"].length > 0) {
|
|
||||||
setChildOrgsAppRuns(childorgappRuns)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubflowRuns(subflowRuns)
|
|
||||||
setWorkflowRuns(workflowRuns)
|
|
||||||
setAppruns(appRuns)
|
|
||||||
setApprunCosts(appcostRuns)
|
|
||||||
}
|
|
||||||
|
|
||||||
const paperStyle = {
|
const paperStyle = {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@@ -708,22 +754,26 @@ const AppStats = (defaultprops) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
} */}
|
} */}
|
||||||
|
|
||||||
{syncStats === true ? null :
|
{/* {syncStats === true ? null : */}
|
||||||
<Tooltip title={
|
<Tooltip title={
|
||||||
<Typography variant="body1" style={{padding: 10, }}>
|
<Typography variant="body1" style={{padding: 10, }}>
|
||||||
App runs in the selected period
|
App runs in the selected period
|
||||||
</Typography>
|
</Typography>
|
||||||
}>
|
}>
|
||||||
<Box sx={paperStyle}>
|
<Box sx={paperStyle}>
|
||||||
|
{syncStats === true ?
|
||||||
|
<Typography variant="h4">
|
||||||
|
{onpremAppRuns}
|
||||||
|
</Typography>:
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
|
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
|
||||||
</Typography>
|
</Typography>}
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
App Runs
|
App Runs
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
{/* } */}
|
||||||
|
|
||||||
{syncStats === true || currentTab === 0 ? null :
|
{syncStats === true || currentTab === 0 ? null :
|
||||||
<Tooltip title={
|
<Tooltip title={
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ const CacheView = memo((props) => {
|
|||||||
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
|
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
|
||||||
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
|
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCategory === "" || selectedCategory === null || selectedCategory === undefined || selectedCategory === "default") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!datastoreCategories.includes(selectedCategory)) {
|
||||||
|
setDatastoreCategories([...datastoreCategories, selectedCategory])
|
||||||
|
}
|
||||||
|
}, [datastoreCategories, selectedCategory])
|
||||||
|
|
||||||
var to_be_copied = "";
|
var to_be_copied = "";
|
||||||
const defaultAutomation = [
|
const defaultAutomation = [
|
||||||
{
|
{
|
||||||
@@ -299,7 +313,16 @@ const CacheView = memo((props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getWorkflows()
|
getWorkflows()
|
||||||
getApps()
|
getApps()
|
||||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
|
||||||
|
var chosenCategory = selectedCategory
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const categoryParam = urlParams.get("category")
|
||||||
|
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
|
||||||
|
chosenCategory = categoryParam
|
||||||
|
setSelectedCategory(categoryParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
listOrgCache(orgId, chosenCategory, 0, pageSize, page)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
@@ -423,7 +446,6 @@ const CacheView = memo((props) => {
|
|||||||
setDatastoreCategories(newcategories)
|
setDatastoreCategories(newcategories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
||||||
|
|
||||||
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
|
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
|
||||||
@@ -458,7 +480,12 @@ const CacheView = memo((props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
|
//toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
|
||||||
|
|
||||||
|
if (category !== undefined && category !== null && category !== "" && category !== "default") {
|
||||||
|
toast.info(`No keys to load in category ${category}`)
|
||||||
|
setSelectedCategory(category)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -513,8 +540,8 @@ const CacheView = memo((props) => {
|
|||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataValue?.category !== "" && dataValue?.category !== "default") {
|
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
|
||||||
entry.category = dataValue.category.replaceAll(" ", "_");
|
entry.category = dataValue?.category?.replaceAll(" ", "_");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +575,10 @@ const CacheView = memo((props) => {
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
setAddCache(responseJson);
|
setAddCache(responseJson);
|
||||||
toast.success("Edit saved");
|
toast.success("Edit saved");
|
||||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
setTimeout(() => {
|
||||||
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
||||||
|
}, 7500);
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -586,7 +616,10 @@ const CacheView = memo((props) => {
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
setAddCache(responseJson);
|
setAddCache(responseJson);
|
||||||
toast.success("New key added!");
|
toast.success("New key added!");
|
||||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
||||||
|
setTimeout(() => {
|
||||||
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
||||||
|
}, 5000);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -1513,7 +1546,7 @@ const CacheView = memo((props) => {
|
|||||||
name={null}
|
name={null}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<Typography variant="body2" style={{maxHeight: 200, overflow: "hidden", }}>
|
<Typography variant="body2" style={{maxWidth: 500, maxHeight: 200, overflow: "auto", }}>
|
||||||
{data.value}
|
{data.value}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
@@ -1712,7 +1745,7 @@ const CacheView = memo((props) => {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{ padding: "6px" }}
|
style={{ padding: "6px" }}
|
||||||
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
|
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false || data.category === "protected"}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -1911,7 +1944,7 @@ const CacheView = memo((props) => {
|
|||||||
|
|
||||||
{selectedCategory === "protected" ?
|
{selectedCategory === "protected" ?
|
||||||
<div style={{ color: red, }}>
|
<div style={{ color: red, }}>
|
||||||
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof.
|
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. If you want unreadable secrets, use <a href="/admin?tab=app_auth" style={{ color: theme.palette.linkColor }}>App Auth</a>.
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
@@ -2051,7 +2084,7 @@ const CacheView = memo((props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
:
|
:
|
||||||
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
|
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
@@ -2077,8 +2110,14 @@ const CacheView = memo((props) => {
|
|||||||
{renderTextBox && <TextField
|
{renderTextBox && <TextField
|
||||||
onKeyPress={(event)=>{
|
onKeyPress={(event)=>{
|
||||||
handleKeyDown(event);
|
handleKeyDown(event);
|
||||||
if(event.key === 'Enter' && selectedFileId.length > 0){
|
|
||||||
|
// Check value of the field
|
||||||
|
const foundValue = event.target.value.trim();
|
||||||
|
if(event.key === 'Enter' && foundValue?.length > 0){
|
||||||
setUpdateToThisCategory(event.target.value)
|
setUpdateToThisCategory(event.target.value)
|
||||||
|
|
||||||
|
listOrgCache(orgId, event.target.value, 0, pageSize, 0)
|
||||||
|
setPage(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
}}
|
}}
|
||||||
@@ -2096,6 +2135,7 @@ const CacheView = memo((props) => {
|
|||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
id=""
|
||||||
color="primary"
|
color="primary"
|
||||||
placeholder="Category name"
|
placeholder="Category name"
|
||||||
required
|
required
|
||||||
@@ -2416,7 +2456,7 @@ const CacheView = memo((props) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
count={Number.parseInt(totalAmount/pageSize*100/2)}
|
count={Number.parseInt(totalAmount/pageSize)}
|
||||||
page={page+1}
|
page={page+1}
|
||||||
renderItem={(item) => {
|
renderItem={(item) => {
|
||||||
var disabled = false
|
var disabled = false
|
||||||
|
|||||||
@@ -18,14 +18,21 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
TextField,
|
TextField,
|
||||||
|
Box,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Rocket as RocketIcon,
|
Rocket as RocketIcon,
|
||||||
FilterAlt as FilterAltIcon,
|
FilterAlt as FilterAltIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
|
Check as CheckIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
green,
|
||||||
|
red,
|
||||||
|
} from '../views/AngularWorkflow.jsx'
|
||||||
|
|
||||||
import algoliasearch from 'algoliasearch/lite';
|
import algoliasearch from 'algoliasearch/lite';
|
||||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||||
|
|
||||||
@@ -97,6 +104,7 @@ const CollectIngestModal = (props) => {
|
|||||||
|
|
||||||
const [showAppsearch, setShowAppsearch] = useState(false);
|
const [showAppsearch, setShowAppsearch] = useState(false);
|
||||||
const [algoliaOptions, setAlgoliaOptions] = useState([]);
|
const [algoliaOptions, setAlgoliaOptions] = useState([]);
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
|
||||||
const appname = type
|
const appname = type
|
||||||
const ingestedAmount = 20
|
const ingestedAmount = 20
|
||||||
@@ -107,7 +115,7 @@ const CollectIngestModal = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var foundMatchingWorkflow = null
|
var foundMatchingWorkflow = null
|
||||||
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
|
if (!showAppsearch && workflows !== undefined && workflows !== null && workflows.length > 0) {
|
||||||
const parsedName = type.toLowerCase().replaceAll(" ", "_");
|
const parsedName = type.toLowerCase().replaceAll(" ", "_");
|
||||||
const foundWorkflow = workflows.find((workflow) => {
|
const foundWorkflow = workflows.find((workflow) => {
|
||||||
return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName
|
return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName
|
||||||
@@ -169,12 +177,36 @@ const CollectIngestModal = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const runIngestion = () => {
|
||||||
|
setGenerating(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setGenerating(false)
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
toast.info("Starting ingest for relevant apps")
|
||||||
|
var newapps = ""
|
||||||
|
for (var key in selectedApps) {
|
||||||
|
const app = selectedApps[key]
|
||||||
|
|
||||||
|
if (newapps.length > 0) {
|
||||||
|
newapps += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
newapps += app.name
|
||||||
|
}
|
||||||
|
|
||||||
|
startIngestion(appname, newapps, appCategory, index)
|
||||||
|
if (webhook === true) {
|
||||||
|
startIngestion(appname+"_webhook", newapps, appCategory, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
//<Grid item xs={hovering ? 12 : 5.9}
|
//<Grid item xs={hovering ? 12 : 5.9}
|
||||||
<Grid item xs={12}
|
<Grid item xs={12}
|
||||||
style={{
|
style={{
|
||||||
minHeight: hovering ? 200 : 200,
|
minHeight: hovering ? 200 : 200,
|
||||||
maxHeight: hovering ? "auto" : 140,
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
transition: "all 0.3s ease-in-out",
|
transition: "all 0.3s ease-in-out",
|
||||||
@@ -200,7 +232,7 @@ const CollectIngestModal = (props) => {
|
|||||||
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
|
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
|
||||||
|
|
||||||
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
|
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
|
||||||
{selectedApps.map((app, index) => {
|
{generating ? null : selectedApps.map((app, index) => {
|
||||||
// Show image of each one
|
// Show image of each one
|
||||||
return (
|
return (
|
||||||
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
|
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
|
||||||
@@ -214,18 +246,29 @@ const CollectIngestModal = (props) => {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Tooltip title="Select Apps" placement="top">
|
{!generating && appCategory !== undefined && appCategory !== null && appCategory.length > 0 ?
|
||||||
<IconButton
|
<Tooltip title={showAppsearch ? "Done selecting apps" : "Select Apps"} placement="top">
|
||||||
style={{marginLeft: 10, marginRight: 50, }}
|
<IconButton
|
||||||
variant="outlined"
|
style={{marginLeft: 10, marginRight: 50, }}
|
||||||
color="secondary"
|
variant="outlined"
|
||||||
onClick={() => {
|
color="secondary"
|
||||||
setShowAppsearch(!showAppsearch)
|
onClick={() => {
|
||||||
}}
|
|
||||||
>
|
if (showAppsearch === true) {
|
||||||
<AddIcon style={{color: theme.palette.primary.main, }} />
|
runIngestion()
|
||||||
</IconButton>
|
}
|
||||||
</Tooltip>
|
|
||||||
|
setShowAppsearch(!showAppsearch)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showAppsearch ?
|
||||||
|
<CheckIcon style={{color: green, }} />
|
||||||
|
:
|
||||||
|
<AddIcon style={{color: theme.palette.primary.main, }} />
|
||||||
|
}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAppsearch ?
|
{showAppsearch ?
|
||||||
@@ -237,20 +280,42 @@ const CollectIngestModal = (props) => {
|
|||||||
|
|
||||||
value={selectedApps}
|
value={selectedApps}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
|
console.log("New value: ", value)
|
||||||
|
|
||||||
setSelectedApps(value)
|
setSelectedApps(value)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
getOptionLabel={(option) => {
|
getOptionLabel={(option) => {
|
||||||
const parsedname = option.name.replaceAll("_", " ")
|
const parsedname = option.name.replaceAll("_", " ")
|
||||||
|
|
||||||
return (
|
return parsedname
|
||||||
<div>
|
//return (
|
||||||
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
|
// <div>
|
||||||
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
|
// <img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
|
||||||
{parsedname}
|
// <Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
|
||||||
</Typography>
|
// {parsedname}
|
||||||
</div>
|
// </Typography>
|
||||||
)
|
// </div>
|
||||||
|
//)
|
||||||
|
}}
|
||||||
|
renderOption={(props, option, state, ownerState) => {
|
||||||
|
const { key, ...optionProps } = props;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={key}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
margin: '5px',
|
||||||
|
padding: '8px',
|
||||||
|
}}
|
||||||
|
component="li"
|
||||||
|
{...optionProps}
|
||||||
|
>
|
||||||
|
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
|
||||||
|
{ownerState.getOptionLabel(option)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => {
|
renderInput={(params) => {
|
||||||
return (
|
return (
|
||||||
@@ -267,24 +332,9 @@ const CollectIngestModal = (props) => {
|
|||||||
style={{width: 250, margin: 25, }}
|
style={{width: 250, margin: 25, }}
|
||||||
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
|
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
runIngestion()
|
||||||
toast.info("Starting ingest for relevant apps")
|
|
||||||
var newapps = ""
|
|
||||||
for (var key in selectedApps) {
|
|
||||||
const app = selectedApps[key]
|
|
||||||
|
|
||||||
if (newapps.length > 0) {
|
|
||||||
newapps += ","
|
|
||||||
}
|
|
||||||
|
|
||||||
newapps += app.name
|
|
||||||
}
|
|
||||||
|
|
||||||
startIngestion(appname, newapps, appCategory, index)
|
|
||||||
if (webhook === true) {
|
|
||||||
startIngestion(appname+"_webhook", newapps, appCategory, index)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
disabled={generating}
|
||||||
>
|
>
|
||||||
{foundMatchingWorkflow !== null ?
|
{foundMatchingWorkflow !== null ?
|
||||||
"Re-Create Ingestion"
|
"Re-Create Ingestion"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
AvatarGroup,
|
AvatarGroup,
|
||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||||
|
|
||||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||||
const CreatorGrid = props => {
|
const CreatorGrid = props => {
|
||||||
@@ -109,6 +110,8 @@ const CreatorGrid = props => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form noValidate action="" role="search">
|
<form noValidate action="" role="search">
|
||||||
<TextField
|
<TextField
|
||||||
@@ -134,7 +137,7 @@ const CreatorGrid = props => {
|
|||||||
id="shuffle_search_field"
|
id="shuffle_search_field"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
removeQuery("q")
|
removeQuery("q")
|
||||||
refine(event.currentTarget.value)
|
debouncedRefine(event.currentTarget.value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if(event.key === "Enter") {
|
if(event.key === "Enter") {
|
||||||
@@ -190,10 +193,10 @@ const CreatorGrid = props => {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
|
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
|
||||||
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
|
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
|
||||||
</Typography>
|
</Typography>
|
||||||
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
|
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
|
||||||
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
|
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
|
||||||
{data.specialized_apps.map((app, index) => {
|
{data.specialized_apps.map((app, index) => {
|
||||||
@@ -267,7 +270,7 @@ const CreatorGrid = props => {
|
|||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={e => setFormMail(e.target.value)}
|
onChange={e => setFormMail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -285,7 +288,7 @@ const CreatorGrid = props => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={e => setMessage(e.target.value)}
|
onChange={e => setMessage(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,515 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
styled,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import theme from "../theme.jsx";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Simple icon placeholders; replace with proper assets if desired
|
||||||
|
const StepIcon = styled("div")(({ completed }) => ({
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 14,
|
||||||
|
color: completed ? "#0C111D" : "#ffffff",
|
||||||
|
background: completed ? "#43D17C" : "#2F2F2F",
|
||||||
|
border: completed ? "1px solid #43D17C" : "1px solid rgba(255,255,255,0.15)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Single continuous rail will be drawn once for the entire steps list
|
||||||
|
|
||||||
|
const StepCard = styled(Box)(({ completed, flash }) => ({
|
||||||
|
display: "flex",
|
||||||
|
gap: "14px",
|
||||||
|
minHeight: 60,
|
||||||
|
backgroundColor: "#212121",
|
||||||
|
border: `1px solid ${flash ? "#f87171" : completed ? "#43D17C" : "rgba(255, 255, 255, 0.1)"}`,
|
||||||
|
borderRadius: "12px",
|
||||||
|
padding: "16px",
|
||||||
|
width: "100%",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const PrimaryButton = styled(Button)({
|
||||||
|
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: 6,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
padding: "8px 20px",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 0.95,
|
||||||
|
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const SecondaryButton = styled(Button)({
|
||||||
|
color: "#FF8544",
|
||||||
|
borderRadius: 6,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
padding: "8px 12px",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const StepItem = React.forwardRef(({ step, iconRef }, ref) => (
|
||||||
|
<Box ref={ref} sx={{ display: "flex", gap: 1.2, alignItems: "flex-start" }}>
|
||||||
|
<Box ref={iconRef} sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: 40, height: 28, zIndex: 1, mt: 1.5 }}>
|
||||||
|
<StepIcon completed={step.completed}>{step.index}</StepIcon>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<StepCard completed={step.completed} flash={Array.isArray(step.flashKeys) && step.flashKeys.includes(step.key)}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between", gap: 1, width: "100%" }}>
|
||||||
|
<Stack direction="column" spacing={1} sx={{ width: "70%" }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</Typography>
|
||||||
|
{step.description && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: "#c5c5c5",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{step.secondaryCta && (
|
||||||
|
<SecondaryButton onClick={step.secondaryCta.onClick}>
|
||||||
|
{step.secondaryCta.label}
|
||||||
|
</SecondaryButton>
|
||||||
|
)}
|
||||||
|
{step.primaryCta && (
|
||||||
|
<PrimaryButton onClick={step.primaryCta.onClick}>
|
||||||
|
{step.primaryCta.label}
|
||||||
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</StepCard>
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
|
||||||
|
const DashboardOnboarding = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
headerTitle = "Get started with your Dashboard",
|
||||||
|
headerSubtitle = "Follow these steps to unlock insights.",
|
||||||
|
footer,
|
||||||
|
globalUrl,
|
||||||
|
onExplore,
|
||||||
|
setOnboardingOpen,
|
||||||
|
isProdStatusOn,
|
||||||
|
isCloud,
|
||||||
|
}) => {
|
||||||
|
// Internal completion state only; handlers are defined separately
|
||||||
|
const [completed, setCompleted] = React.useState({
|
||||||
|
docs: false,
|
||||||
|
apps: false,
|
||||||
|
workflow: false,
|
||||||
|
wait: false,
|
||||||
|
invite: false,
|
||||||
|
});
|
||||||
|
const [checkingApps, setCheckingApps] = React.useState(false);
|
||||||
|
const [checkingWait, setCheckingWait] = React.useState(false);
|
||||||
|
const [flashKeys, setFlashKeys] = React.useState([]);
|
||||||
|
const [waitProgress, setWaitProgress] = React.useState(0);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Load persisted completion state
|
||||||
|
React.useEffect(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("dashboard_onboarding_completed");
|
||||||
|
if (!raw) return;
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
if (data && typeof data === "object") {
|
||||||
|
setCompleted((prev) => ({ ...prev, ...data }));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Persist completion state
|
||||||
|
React.useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("dashboard_onboarding_completed", JSON.stringify(completed));
|
||||||
|
} catch {}
|
||||||
|
}, [completed]);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleDocsClick = React.useCallback(() => {
|
||||||
|
window.open('/docs', '_blank');
|
||||||
|
setCompleted((prev) => ({ ...prev, docs: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDiscoverApps = React.useCallback(() => {
|
||||||
|
window.open('/apps?tab=discover_apps', '_blank');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCheckAppsStatus = React.useCallback(async () => {
|
||||||
|
if (checkingApps) return;
|
||||||
|
setCheckingApps(true);
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${globalUrl}/api/v1/apps`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status !== 200) return;
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
let count = 0;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
count = data.length;
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
count = Object.keys(data).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= 3) {
|
||||||
|
setCompleted((prev) => ({ ...prev, apps: true }));
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setCheckingApps(false);
|
||||||
|
}
|
||||||
|
}, [checkingApps]);
|
||||||
|
|
||||||
|
const handleOpenWorkflow = React.useCallback(() => {
|
||||||
|
window.open('/workflows/b658f2a0-7316-40d9-97ed-350a54fe3adc', '_blank');
|
||||||
|
setCompleted((prev) => ({ ...prev, workflow: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWaitCheck = React.useCallback(async () => {
|
||||||
|
if (checkingWait) return;
|
||||||
|
setCheckingWait(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const days = 5;
|
||||||
|
const url = `${globalUrl}/api/v1/stats/workflow_executions_finished?days=${days}`;
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) return;
|
||||||
|
const data = await resp.json();
|
||||||
|
const entries = Array.isArray(data?.entries) ? data.entries : [];
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let successDays = 0;
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const e = entries[i];
|
||||||
|
const d = new Date(e?.Date || e?.date || e?.time || e?.timestamp);
|
||||||
|
const diffDays = Math.floor((now - d) / (24 * 60 * 60 * 1000));
|
||||||
|
const value = Number(e?.Value ?? e?.value ?? 0);
|
||||||
|
if (!isNaN(diffDays) && diffDays <= 4 && value > 0) {
|
||||||
|
successDays += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const simulated = Math.min(5, successDays);
|
||||||
|
if (simulated < 5) {
|
||||||
|
setWaitProgress(simulated);
|
||||||
|
setFlashKeys(["wait"]);
|
||||||
|
setTimeout(() => setFlashKeys([]), 800);
|
||||||
|
setCheckingWait(false);
|
||||||
|
} else {
|
||||||
|
setCompleted((prev) => ({ ...prev, wait: true }));
|
||||||
|
setCheckingWait(false);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setCheckingWait(false);
|
||||||
|
}
|
||||||
|
setCheckingWait(false);
|
||||||
|
}, [checkingWait, globalUrl]);
|
||||||
|
|
||||||
|
const handleOpenUsers = React.useCallback(() => {
|
||||||
|
window.open('/admin?tab=users', '_blank');
|
||||||
|
setCompleted((prev) => ({ ...prev, invite: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
key: 'docs',
|
||||||
|
title: 'Read our docs to understand Shuffle',
|
||||||
|
description: 'Explore the basics of Shuffle in our documentation. It will help you understand the platform and how to use it.',
|
||||||
|
primaryCta: { label: 'Read docs', onClick: handleDocsClick },
|
||||||
|
completed: completed.docs,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
key: 'apps',
|
||||||
|
title: 'Activate at least 3 apps',
|
||||||
|
description: 'Go to Discover Apps and enable your first three integrations.',
|
||||||
|
primaryCta: { label: 'Discover apps', onClick: handleDiscoverApps },
|
||||||
|
secondaryCta: { label: checkingApps ? 'Checking…' : 'Check status', onClick: handleCheckAppsStatus, disabled: checkingApps },
|
||||||
|
completed: completed.apps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 3,
|
||||||
|
key: 'workflow',
|
||||||
|
title: 'Save a public workflow and start its scheduler',
|
||||||
|
description: 'Open the public workflow, save it to your org, and start a daily scheduler.',
|
||||||
|
primaryCta: { label: 'Open public workflow', onClick: handleOpenWorkflow },
|
||||||
|
completed: completed.workflow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 4,
|
||||||
|
key: 'wait',
|
||||||
|
title: `Wait for 5 days of runs${completed.wait ? '' : waitProgress > 0 ? ` (${waitProgress}/5)` : ''}`,
|
||||||
|
description: 'We will show daily stats after 5 runs. Come back to check again.',
|
||||||
|
primaryCta: { label: checkingWait ? 'Checking…' : 'Check status', onClick: handleWaitCheck, disabled: checkingWait },
|
||||||
|
completed: completed.wait,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 5,
|
||||||
|
key: 'invite',
|
||||||
|
title: 'Invite your team members',
|
||||||
|
description: 'Add teammates to collaborate in your org.',
|
||||||
|
primaryCta: { label: 'Open users page', onClick: handleOpenUsers },
|
||||||
|
completed: completed.invite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mandatoryKeys = ['docs', 'apps', 'workflow', 'wait'];
|
||||||
|
const handleFinalDone = React.useCallback(() => {
|
||||||
|
const missing = mandatoryKeys.filter((k) => !completed[k]);
|
||||||
|
if (missing.length === 0) {
|
||||||
|
try { localStorage.setItem("dashboard_onboarding_complete", "true"); } catch {}
|
||||||
|
if (typeof onExplore === 'function') {
|
||||||
|
try { onExplore(); } catch {}
|
||||||
|
}
|
||||||
|
if (onClose) onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlashKeys(missing);
|
||||||
|
setTimeout(() => setFlashKeys([]), 800);
|
||||||
|
}, [completed, onClose, onExplore]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000, }}>
|
||||||
|
{/* Blur overlay with visible background */}
|
||||||
|
<Box
|
||||||
|
onClick={onClose}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
backdropFilter: "blur(6px)",
|
||||||
|
background: "rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal container */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 2001,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 780,
|
||||||
|
backgroundColor: "#1A1A1A",
|
||||||
|
borderRadius: "16px",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
p: 3,
|
||||||
|
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
|
||||||
|
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: "#f1f1f1",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 22,
|
||||||
|
letterSpacing: "-0.2px",
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headerTitle}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: "#c5c5c5",
|
||||||
|
mt: 0.5,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headerSubtitle}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!isCloud ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 20,
|
||||||
|
padding: "4px 10px",
|
||||||
|
marginLeft: "5px",
|
||||||
|
marginRight: "5px",
|
||||||
|
borderRadius: 20,
|
||||||
|
marginBottom: "14px",
|
||||||
|
background: isProdStatusOn
|
||||||
|
? "rgba(43, 192, 126, 0.1)"
|
||||||
|
: "rgba(255, 82, 82, 0.1)",
|
||||||
|
cursor: "pointer",
|
||||||
|
|
||||||
|
position: "absolute",
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/admin?admin_tab=billingstats")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
marginLeft: 10,
|
||||||
|
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||||
|
borderRadius: 999,
|
||||||
|
display: "inline",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
style={{
|
||||||
|
fontFamily: "12px",
|
||||||
|
opacity: 0.9,
|
||||||
|
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isProdStatusOn ? "Production" : "NOT Production"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Steps list with a single continuous rail */}
|
||||||
|
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
|
||||||
|
{/* Base grey rail */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 20,
|
||||||
|
top: 30,
|
||||||
|
bottom: 60,
|
||||||
|
width: 2,
|
||||||
|
background: "rgba(255,255,255,0.2)",
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Green segments between consecutive completed steps */}
|
||||||
|
{steps.map((s, i) => ({ s, i }))
|
||||||
|
.filter(({ i }) => i < steps.length - 1)
|
||||||
|
.filter(({ i }) => steps[i].completed && steps[i + 1].completed)
|
||||||
|
.map(({ i }) => (
|
||||||
|
<Box
|
||||||
|
key={`seg-${i}`}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 20,
|
||||||
|
top: 30 + i * 110, // approximate segment height per step
|
||||||
|
height: 130, // matches gap+card combined height; tuned visually
|
||||||
|
width: 2,
|
||||||
|
background: "#43D17C",
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{steps.map((step, idx) => (
|
||||||
|
<StepItem key={step.key || idx} step={{...step, flashKeys}} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
|
||||||
|
{footer}
|
||||||
|
<Button variant="contained" color="primary" onClick={handleFinalDone}
|
||||||
|
sx={{
|
||||||
|
fontSize: 14,
|
||||||
|
padding: "8px 60px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Explore
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="text" color="secondary" onClick={() => {
|
||||||
|
setOnboardingOpen(false)
|
||||||
|
|
||||||
|
toast.warn("The dashboard is not fully set up yet. You can complete the steps later from the onboarding section.", { timeout: 10000 })
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
fontSize: 14,
|
||||||
|
padding: "8px 60px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardOnboarding;
|
||||||
|
|
||||||
|
|
||||||
@@ -143,6 +143,7 @@ const Detection = (props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ mr: 2 }}
|
sx={{ mr: 2 }}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
disabled
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{/* <Button
|
{/* <Button
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
OpenInNew as OpenInNewIcon,
|
OpenInNew as OpenInNewIcon,
|
||||||
FmdGood as FmdGoodIcon,
|
FmdGood as FmdGoodIcon,
|
||||||
|
Check as CheckIcon,
|
||||||
} from "@mui/icons-material"
|
} from "@mui/icons-material"
|
||||||
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import RunDetectionTest from '../components/RunDetectionTest.jsx';
|
||||||
import theme from '../theme.jsx';
|
import theme from '../theme.jsx';
|
||||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||||
|
import CollectIngestModal from "../components/CollectIngestModal.jsx";
|
||||||
import {
|
import {
|
||||||
green,
|
green,
|
||||||
red,
|
red,
|
||||||
@@ -30,13 +33,12 @@ import {
|
|||||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||||
|
|
||||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
|
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
|
||||||
if (!isDetectionActive) {
|
//if (!isDetectionActive) {
|
||||||
toast.warn("Connect to siem first for global enable/disable to work");
|
// toast.warn("Connect first for global enable/disable to work");
|
||||||
return;
|
// return;
|
||||||
}
|
//}
|
||||||
|
|
||||||
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
||||||
//const url = `${globalUrl}/api/v1/detections/${detectionType}/selected_rules/${action}`;
|
|
||||||
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
|
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
@@ -68,10 +70,117 @@ const DetectionExplorer = (props) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [workflow, setWorkflow] = useState({})
|
const [workflow, setWorkflow] = useState({})
|
||||||
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
|
|
||||||
const [isDetectionValid, setIsDetectionValid] = useState(false)
|
|
||||||
const [availableDetection, setAvailableDetection] = React.useState([]);
|
const [availableDetection, setAvailableDetection] = React.useState([]);
|
||||||
const [environmentList, setEnvironmentList] = React.useState([])
|
const [environmentList, setEnvironmentList] = React.useState([])
|
||||||
|
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
|
||||||
|
const [workflows, setWorkflows] = React.useState([])
|
||||||
|
const [apps, setApps] = React.useState([])
|
||||||
|
const [pipelines, setPipelines] = React.useState([])
|
||||||
|
|
||||||
|
const [ticketWebhook, setTicketWebhook] = React.useState("");
|
||||||
|
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
|
||||||
|
|
||||||
|
const handleGetAllTriggers = () => {
|
||||||
|
fetch(globalUrl + "/api/v1/triggers", {
|
||||||
|
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) => {
|
||||||
|
//setWebHooks(responseJson.webhooks || []);
|
||||||
|
//setAllSchedules(responseJson.schedules || []);
|
||||||
|
setPipelines(responseJson.pipelines || []);
|
||||||
|
//setShowLoader(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// toast(error.toString());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWorkflows = () => {
|
||||||
|
const url = `${globalUrl}/api/v1/workflows`
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for workflows :O!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson?.success !== false) {
|
||||||
|
setWorkflows(responseJson || []);
|
||||||
|
|
||||||
|
for (var i = 0; i < responseJson?.length; i++) {
|
||||||
|
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
|
||||||
|
|
||||||
|
for (var triggerkey in responseJson[i].triggers) {
|
||||||
|
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}`)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApps = () => {
|
||||||
|
const url = `${globalUrl}/api/v1/apps`
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for workflows :O!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson?.success === false) {
|
||||||
|
toast.warn("Failed to load apps. Please try again or contact support@shuffler if this persists.")
|
||||||
|
} else {
|
||||||
|
setApps(responseJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const loadUsecases = () => {
|
const loadUsecases = () => {
|
||||||
const url = `${globalUrl}/api/v1/workflows/usecases`
|
const url = `${globalUrl}/api/v1/workflows/usecases`
|
||||||
@@ -133,6 +242,11 @@ const DetectionExplorer = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConnectClick = () => {
|
const handleConnectClick = () => {
|
||||||
|
|
||||||
|
// NEW way to handle it
|
||||||
|
setShowCollectIngestMenu(true)
|
||||||
|
return
|
||||||
|
|
||||||
if (detectionWorkflowId !== "") {
|
if (detectionWorkflowId !== "") {
|
||||||
console.log("Already have a workflow ID for this detection")
|
console.log("Already have a workflow ID for this detection")
|
||||||
//toast.info(`Already have a detection workflow for ${detectionInfo?.category}`)
|
//toast.info(`Already have a detection workflow for ${detectionInfo?.category}`)
|
||||||
@@ -179,7 +293,7 @@ const DetectionExplorer = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
|
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
|
||||||
setIsDetectionValid(responseJson.workflow_valid)
|
//setIsDetectionValid(responseJson.workflow_valid)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||||
@@ -238,8 +352,12 @@ const DetectionExplorer = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
getApps()
|
||||||
|
getWorkflows()
|
||||||
loadUsecases()
|
loadUsecases()
|
||||||
loadEnvironments()
|
loadEnvironments()
|
||||||
|
|
||||||
|
handleGetAllTriggers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -251,18 +369,87 @@ const DetectionExplorer = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnectClick()
|
console.log("Detection info: ", detectionInfo)
|
||||||
|
//handleConnectClick()
|
||||||
}, [detectionInfo])
|
}, [detectionInfo])
|
||||||
|
|
||||||
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
|
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
|
||||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
rule?.file_name?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
|
||||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
rule?.title?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
|
||||||
|
rule?.description?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery)
|
||||||
)
|
)
|
||||||
|
|
||||||
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
|
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
|
||||||
|
|
||||||
|
const submitPipeline = (pipeline, environment) => {
|
||||||
|
var pipelineConfig = {
|
||||||
|
command: pipeline,
|
||||||
|
name: pipeline,
|
||||||
|
type: "create",
|
||||||
|
environment: "",
|
||||||
|
|
||||||
|
workflow_id: "",
|
||||||
|
trigger_id: "",
|
||||||
|
start_node: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment !== undefined && environment !== "") {
|
||||||
|
pipelineConfig.environment = environment
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${globalUrl}/api/v1/triggers/pipeline`;
|
||||||
|
fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(pipelineConfig),
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for stream results :O!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (!responseJson.success && pipelineConfig.type !== "delete") {
|
||||||
|
toast.error("Failed to set pipeline: " + responseJson.reason);
|
||||||
|
} else {
|
||||||
|
if (pipelineConfig.type === "create") {
|
||||||
|
toast.success("Pipeline will be created: " + responseJson.reason)
|
||||||
|
//setPipelineModalOpen(false)
|
||||||
|
|
||||||
|
} else if (pipelineConfig.type === "stop") {
|
||||||
|
toast.success("Pipeline will be stopped: " + responseJson.reason)
|
||||||
|
//setPipelineModalOpen(false)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.info("Unknown pipeline type: " + pipelineConfig.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Get pipeline error: ", error.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|
||||||
|
<CollectIngestModal
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
open={showCollectIngestMenu}
|
||||||
|
setOpen={setShowCollectIngestMenu}
|
||||||
|
|
||||||
|
workflows={workflows}
|
||||||
|
getWorkflows={getWorkflows}
|
||||||
|
|
||||||
|
apps={apps}
|
||||||
|
/>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
style={{
|
style={{
|
||||||
marginTop: 50,
|
marginTop: 50,
|
||||||
@@ -316,8 +503,21 @@ const DetectionExplorer = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
: */}
|
: */}
|
||||||
|
<div style={{marginRight: 20, }}>
|
||||||
|
<RunDetectionTest
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
pipelines={pipelines}
|
||||||
|
workflows={workflows}
|
||||||
|
ticketWebhook={ticketWebhook}
|
||||||
|
detectionWorkflowId={detectionWorkflowId}
|
||||||
|
|
||||||
|
changePipelineState={undefined}
|
||||||
|
submitPipelineWrapper={submitPipeline}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant={detectionWorkflowId === "" ? "contained" : "outlined"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleConnectClick()
|
handleConnectClick()
|
||||||
}}
|
}}
|
||||||
@@ -326,18 +526,26 @@ const DetectionExplorer = (props) => {
|
|||||||
// Red = workflow exists, validation is false
|
// Red = workflow exists, validation is false
|
||||||
// Green = workflow exists, validation is true
|
// Green = workflow exists, validation is true
|
||||||
// Grey = workflow does not exist
|
// Grey = workflow does not exist
|
||||||
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? <CircularProgress size={24} /> :
|
{loading ?
|
||||||
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
|
<CircularProgress size={24} />
|
||||||
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
|
:
|
||||||
|
detectionWorkflowId !== "" ?
|
||||||
|
<span>
|
||||||
|
<CheckIcon style={{color: green, marginRight: 10, top: 5, }} />
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
:
|
||||||
|
`Connect to ${detectionInfo?.category}`
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/**/}
|
{/**/}
|
||||||
|
|
||||||
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
|
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
|
||||||
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
|
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
|
||||||
<a href="/admin?tab=Locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
|
<a href="/admin?tab=locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
|
||||||
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
|
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -345,7 +553,7 @@ const DetectionExplorer = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
{filteredRules?.length > 0 ?
|
{ruleInfo?.length > 0 ?
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -367,7 +575,9 @@ const DetectionExplorer = (props) => {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ mr: 2 }}
|
sx={{ mr: 2 }}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e?.target?.value?.replaceAll(" ", "_")?.toLowerCase())
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
@@ -386,7 +596,7 @@ const DetectionExplorer = (props) => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "500px",
|
minHeight: 500,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
p: 1,
|
p: 1,
|
||||||
@@ -410,6 +620,7 @@ const DetectionExplorer = (props) => {
|
|||||||
folderDisabled={folderDisabled}
|
folderDisabled={folderDisabled}
|
||||||
isDetectionActive={isDetectionActive}
|
isDetectionActive={isDetectionActive}
|
||||||
|
|
||||||
|
ruleDetails={rule}
|
||||||
ruleMapping={ruleMapping}
|
ruleMapping={ruleMapping}
|
||||||
setRuleMapping={setRuleMapping}
|
setRuleMapping={setRuleMapping}
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
import LineChartWrapper, { LoadStats } from "../components/LineChartWrapper.jsx";
|
||||||
import {
|
import {
|
||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||||
import theme from '../theme.jsx';
|
import theme from '../theme.jsx';
|
||||||
|
|
||||||
|
const RuleCard = (props) => {
|
||||||
|
const { ruleName, description, file_id, globalUrl, folderDisabled, isDetectionActive, availableDetection, ruleMapping, setRuleMapping, ruleDetails, key, ...otherProps } = props
|
||||||
|
|
||||||
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
|
|
||||||
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
|
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
|
||||||
const [fileData, setFileData] = React.useState("");
|
const [fileData, setFileData] = React.useState("");
|
||||||
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
|
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
|
||||||
@@ -30,35 +32,33 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
const [responseValue, setResponseValue] = React.useState("No response action")
|
const [responseValue, setResponseValue] = React.useState("No response action")
|
||||||
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
|
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
|
||||||
|
|
||||||
console.log("Rulemapping: ", ruleMapping)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (key < 10) {
|
||||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
console.log("RuleCard Key: ", key, ruleName, file_id, otherProps)
|
||||||
//const resp = LoadStats(globalUrl, ruleName)
|
|
||||||
//const resp = LoadStats(globalUrl, "app_executions_test2")
|
|
||||||
const resp = LoadStats(globalUrl, "app_executions_cloud")
|
|
||||||
resp.then((data) => {
|
|
||||||
if (data === undefined) {
|
|
||||||
setFilteredBarchart([])
|
|
||||||
} else {
|
|
||||||
setFilteredBarchart(data)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
|
if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
|
||||||
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
|
//toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
|
||||||
|
resp.then((data) => {
|
||||||
|
if (data === undefined) {
|
||||||
|
setFilteredBarchart([])
|
||||||
|
} else {
|
||||||
|
setFilteredBarchart(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
console.log("Response Value: ", responseValue)
|
|
||||||
|
|
||||||
const handleSwitchChange = (event) => {
|
const handleSwitchChange = (event) => {
|
||||||
if (folderDisabled) {
|
if (folderDisabled) {
|
||||||
toast.warn("Enable the directory to enable individual rules");
|
toast.warn("Enable the directory to enable individual rules");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTenzirActive) {
|
if (!isDetectionActive) {
|
||||||
toast.warn("Connect to the siem first to enable/disable the rule");
|
toast.warn("Connect to the siem first to enable/disable the rule");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,6 +96,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var parsedRulename = ruleName.charAt(0).toUpperCase() + ruleName.slice(1).replaceAll("_", " ")
|
||||||
return (
|
return (
|
||||||
<Card style={{
|
<Card style={{
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
@@ -116,10 +117,13 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
color: "white",
|
color: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography>
|
<Typography variant="h6">
|
||||||
|
{parsedRulename} {/*({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})*/}
|
||||||
|
</Typography>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
|
||||||
<Select
|
{/*
|
||||||
|
<Select
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
disableScrollLock: true,
|
disableScrollLock: true,
|
||||||
}}
|
}}
|
||||||
@@ -148,6 +152,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
color: "white",
|
color: "white",
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
|
marginRight: 20,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -178,6 +183,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
|
*/}
|
||||||
|
|
||||||
|
|
||||||
<Tooltip title="Edit Rule" placement="top">
|
<Tooltip title="Edit Rule" placement="top">
|
||||||
@@ -204,12 +210,47 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
|||||||
|
|
||||||
minHeight: 40,
|
minHeight: 40,
|
||||||
maxHeight: 40,
|
maxHeight: 40,
|
||||||
|
display: "flex",
|
||||||
}}>
|
}}>
|
||||||
{filteredBarchart === null ? null :
|
<Tooltip title="Refresh stats" placement="top">
|
||||||
<DashboardBarchart
|
<IconButton
|
||||||
timelineData={filteredBarchart}
|
onClick={() => {
|
||||||
/>
|
if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
|
||||||
|
toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
|
||||||
|
resp.then((data) => {
|
||||||
|
console.log("DATA: ", data)
|
||||||
|
if (data === undefined) {
|
||||||
|
setFilteredBarchart([])
|
||||||
|
} else {
|
||||||
|
setFilteredBarchart(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshIcon
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{filteredBarchart === null ? <Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginLeft: 18, }}>No stats yet</Typography> :
|
||||||
|
<div style={{minWidth: "90%", }}>
|
||||||
|
<LineChartWrapper
|
||||||
|
inputname={""}
|
||||||
|
keys={filteredBarchart}
|
||||||
|
height={100}
|
||||||
|
width={"100%"}
|
||||||
|
border={false}
|
||||||
|
|
||||||
|
color={"#808080"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Search as SearchIcon } from '@mui/icons-material';
|
import { Search as SearchIcon } from '@mui/icons-material';
|
||||||
|
import useDebouncedCallback from '../utils/useDebouncedCallback.jsx';
|
||||||
|
|
||||||
|
|
||||||
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
|
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
|
||||||
@@ -69,12 +70,22 @@ const DiscordChat = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SearchBox = ({ currentRefinement, refine }) => {
|
const SearchBox = ({ currentRefinement, refine }) => {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(currentRefinement || "");
|
||||||
|
}, [currentRefinement]);
|
||||||
return (
|
return (
|
||||||
<form noValidate action="" role="search">
|
<form noValidate action="" role="search">
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
value={currentRefinement}
|
value={inputValue}
|
||||||
onChange={(event) => refine(event.currentTarget.value)}
|
onChange={(event) => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
setInputValue(value);
|
||||||
|
debouncedRefine(value);
|
||||||
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if(event.key === "Enter") {
|
if(event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -97,6 +98,8 @@ const DocsGrid = props => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form noValidate action="" role="search">
|
<form noValidate action="" role="search">
|
||||||
<TextField
|
<TextField
|
||||||
@@ -122,7 +125,7 @@ const DocsGrid = props => {
|
|||||||
id="shuffle_search_field"
|
id="shuffle_search_field"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
removeQuery("q")
|
removeQuery("q")
|
||||||
refine(event.currentTarget.value)
|
debouncedRefine(event.currentTarget.value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if(event.key === "Enter") {
|
if(event.key === "Enter") {
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ const EditWorkflow = (props) => {
|
|||||||
submitLoading === true ||
|
submitLoading === true ||
|
||||||
(uploadedImage === null && innerWorkflow?.default_return_value?.trim()?.length === 0)
|
(uploadedImage === null && innerWorkflow?.default_return_value?.trim()?.length === 0)
|
||||||
}
|
}
|
||||||
variant="aiButton"
|
variant="aiButtonGhost"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// Track AI Generate button click
|
// Track AI Generate button click
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
@@ -917,10 +917,10 @@ const EditWorkflow = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h6" style={{ marginBottom: 5 }}>
|
<Typography variant="h6" style={{ marginBottom: 5 }}>
|
||||||
Generate Workflow from Flowchart
|
Generate Workflow from Flowchart (beta)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
|
||||||
Click to upload your flowchart - AI will convert it to a workflow
|
Click to upload your flowchart - Your LLM will convert it to a workflow
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="textSecondary">
|
<Typography variant="caption" color="textSecondary">
|
||||||
PNG, JPG, JPEG • Max 5MB
|
PNG, JPG, JPEG • Max 5MB
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
Help as HelpIcon,
|
Help as HelpIcon,
|
||||||
ExpandLess as ExpandLessIcon,
|
ExpandLess as ExpandLessIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
ExpandMore as ExpandMoreIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Context } from '../context/ContextApi.jsx';
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
@@ -53,7 +54,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
pipelines: false,
|
pipelines: false,
|
||||||
proxies: false,
|
proxies: false,
|
||||||
})
|
})
|
||||||
const [installationTab, setInstallationTab] = React.useState(0);
|
const [installationTab, setInstallationTab] = React.useState(1);
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
const [listItemExpanded, setListItemExpanded] = React.useState(-1);
|
const [listItemExpanded, setListItemExpanded] = React.useState(-1);
|
||||||
const [, setUpdate] = React.useState(0);
|
const [, setUpdate] = React.useState(0);
|
||||||
@@ -61,6 +62,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
const [selectedEnvironment, setSelectedEnvironment] = React.useState(null);
|
const [selectedEnvironment, setSelectedEnvironment] = React.useState(null);
|
||||||
const [selectedSubOrg, setSelectedSubOrg] = React.useState([]);
|
const [selectedSubOrg, setSelectedSubOrg] = React.useState([]);
|
||||||
const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined)
|
const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined)
|
||||||
|
const [currentEnvQueue, setCurrentEnvQueue] = React.useState([])
|
||||||
|
|
||||||
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
||||||
const theme = getTheme(themeMode, brandColor);
|
const theme = getTheme(themeMode, brandColor);
|
||||||
@@ -408,6 +410,11 @@ const EnvironmentTab = memo((props) => {
|
|||||||
skipPipeline = true
|
skipPipeline = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showDetection = false
|
||||||
|
if (commandController.detection === true) {
|
||||||
|
showDetection = true
|
||||||
|
}
|
||||||
|
|
||||||
var addProxy = false
|
var addProxy = false
|
||||||
if (commandController.proxies === true) {
|
if (commandController.proxies === true) {
|
||||||
addProxy = true
|
addProxy = true
|
||||||
@@ -422,12 +429,11 @@ const EnvironmentTab = memo((props) => {
|
|||||||
-e AUTH="${auth}" \\
|
-e AUTH="${auth}" \\
|
||||||
-e ENVIRONMENT_NAME="${environment.Name}" \\
|
-e ENVIRONMENT_NAME="${environment.Name}" \\
|
||||||
-e ORG="${environment.org_id}" \\
|
-e ORG="${environment.org_id}" \\
|
||||||
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\
|
|
||||||
-e SHUFFLE_SWARM_CONFIG=run \\
|
-e SHUFFLE_SWARM_CONFIG=run \\
|
||||||
-e SHUFFLE_LOGS_DISABLED=true \\
|
|
||||||
-e BASE_URL="${newUrl}" \\${addProxy ? `
|
-e BASE_URL="${newUrl}" \\${addProxy ? `
|
||||||
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
||||||
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
|
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
|
||||||
|
-v /tmp:/tmp \\` : ""}
|
||||||
ghcr.io/shuffle/shuffle-orborus:latest
|
ghcr.io/shuffle/shuffle-orborus:latest
|
||||||
`)
|
`)
|
||||||
} else if (installationTab === 2) {
|
} else if (installationTab === 2) {
|
||||||
@@ -676,6 +682,76 @@ const EnvironmentTab = memo((props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const removeEnvQueueItem = (environment, queueItem) => {
|
||||||
|
const url = `${globalUrl}/api/v1/workflows/queue/confirm`
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Org-Id": environment.Name,
|
||||||
|
"Org": environment.org_id,
|
||||||
|
"Authorization": environment.auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = {
|
||||||
|
"data": [queueItem],
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(items),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for apps :O!");
|
||||||
|
toast("Failed removing queue item")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson?.success !== false) {
|
||||||
|
toast("Successfully removed queue item")
|
||||||
|
getEnvQueue(environment)
|
||||||
|
} else {
|
||||||
|
toast("Failed removing queue item")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEnvQueue = (environment) => {
|
||||||
|
const url = `${globalUrl}/api/v1/workflows/queue`
|
||||||
|
const headers = {
|
||||||
|
"Org-Id": environment.Name,
|
||||||
|
"Org": environment.org_id,
|
||||||
|
"Authorization": environment.auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for apps :O!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson?.success !== false && responseJson?.data?.length > 0) {
|
||||||
|
setCurrentEnvQueue(responseJson.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => {
|
const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => {
|
||||||
const data = {
|
const data = {
|
||||||
action: "suborg_distribute",
|
action: "suborg_distribute",
|
||||||
@@ -881,14 +957,14 @@ const EnvironmentTab = memo((props) => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
|
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
borderBottom: theme.palette.defaultBorder,
|
borderBottom: theme.palette.defaultBorder,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => {
|
{["Type", "Status", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemText
|
<ListItemText
|
||||||
@@ -912,7 +988,6 @@ const EnvironmentTab = memo((props) => {
|
|||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
|
|
||||||
backgroundColor: theme.palette.platformColor,
|
backgroundColor: theme.palette.platformColor,
|
||||||
height: 40,
|
height: 40,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -1014,12 +1089,17 @@ const EnvironmentTab = memo((props) => {
|
|||||||
key={index}
|
key={index}
|
||||||
style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0, borderBottomLeftRadius: environments?.length - 1 === index ? 8 : 0, borderBottomRightRadius: environments?.length - 1 === index ? 8 : 0, display: 'grid', gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 405px 125px", }}
|
style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0, borderBottomLeftRadius: environments?.length - 1 === index ? 8 : 0, borderBottomRightRadius: environments?.length - 1 === index ? 8 : 0, display: 'grid', gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 405px 125px", }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (environment.Type === "cloud") {
|
if (environment.Type === "cloud") {
|
||||||
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
|
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setListItemExpanded(listItemExpanded === index ? -1 : index)
|
setListItemExpanded(listItemExpanded === index ? -1 : index)
|
||||||
|
|
||||||
|
if (listItemExpanded !== index) {
|
||||||
|
getEnvQueue(environment)
|
||||||
|
setCurrentEnvQueue([])
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
@@ -1100,7 +1180,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"} {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"}
|
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"}. {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"}
|
||||||
</Typography>
|
</Typography>
|
||||||
} placement="top">
|
} placement="top">
|
||||||
<Typography
|
<Typography
|
||||||
@@ -1182,42 +1262,14 @@ const EnvironmentTab = memo((props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
|
|
||||||
"N/A"
|
|
||||||
:
|
|
||||||
environment.licensed ? (
|
|
||||||
<Tooltip title="Scale configured (auto on cloud)" placement="top">
|
|
||||||
<CheckCircleIcon style={{ color: "#4caf50" }} />
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
title="In Verbose mode. Set SHUFFLE_SWARM_CONFIG=run to Scale. This will not be as verbose. Details: https://shuffler.io/docs/configuration#scaling-shuffle"
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/docs/configuration#scaling-shuffle"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<CancelIcon style={{ color: "#f85a3e" }} />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
minWidth: 60,
|
|
||||||
marginLeft: 20,
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
wordWrap: "break-word",
|
|
||||||
padding: 8,
|
|
||||||
display: "table-cell",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
|
style={{
|
||||||
|
marginLeft: 30,
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
display: "table-cell",
|
||||||
|
}}
|
||||||
primary={
|
primary={
|
||||||
environment.Type === "cloud" ?
|
environment.Type === "cloud" ?
|
||||||
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
|
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
|
||||||
@@ -1231,40 +1283,32 @@ const EnvironmentTab = memo((props) => {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|
||||||
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
|
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
|
||||||
<CheckCircleIcon style={{ color: "#4caf50" }} />
|
<CheckCircleIcon style={{ color: "#4caf50" }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Data Lake node disabled. Click to enable."
|
title="Data Lake node disabled. Click to enable."
|
||||||
placement="top"
|
placement="top"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
window.open("/detections/Sigma", "_blank")
|
window.open("/detections/Sigma", "_blank")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/detections/Sigma"
|
href="/detections/Sigma"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<CancelIcon style={{ color: "#f85a3e" }} />
|
<CancelIcon style={{ color: "#f85a3e" }} />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
style={{
|
/>
|
||||||
minWidth: 60,
|
|
||||||
marginLeft: 40,
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
wordWrap: "break-word",
|
|
||||||
display: "table-cell",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={(
|
primary={(
|
||||||
@@ -1274,12 +1318,12 @@ const EnvironmentTab = memo((props) => {
|
|||||||
)}
|
)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
style:{
|
style:{
|
||||||
maxWidth: 150,
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
wordWrap: "break-word",
|
wordWrap: "break-word",
|
||||||
transition: "all 0.3s ease",
|
transition: "all 0.3s ease",
|
||||||
|
textAlign: "center",
|
||||||
}}}
|
}}}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
@@ -1292,7 +1336,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
primary={environment.Type}
|
primary={environment.Type}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
style:{
|
style:{
|
||||||
minWidth: 70,
|
minWidth: 50,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
@@ -1302,6 +1346,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
}}}
|
}}}
|
||||||
style={{display: "table-cell",}}
|
style={{display: "table-cell",}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
style:{
|
style:{
|
||||||
@@ -1344,7 +1389,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
Make Default
|
Default
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={environment.archived ? "contained" : "outlined"}
|
variant={environment.archived ? "contained" : "outlined"}
|
||||||
@@ -1418,34 +1463,38 @@ const EnvironmentTab = memo((props) => {
|
|||||||
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
|
{/*
|
||||||
<IconButton disabled={environment.Type === "cloud"} onClick={()=> {setIsExpanded(prev => !prev)}}>
|
<IconButton disabled={environment.Type === "cloud"} onClick={()=> {setIsExpanded(prev => !prev)}}>
|
||||||
{listItemExpanded === index ? <ExpandLessIcon sx={{color: theme.palette.text.primary}} /> : <ExpandMoreIcon sx={{color: theme.palette.text.primary}}/>}
|
{listItemExpanded === index ? <ExpandLessIcon sx={{color: theme.palette.text.primary}} /> : <ExpandMoreIcon sx={{color: theme.palette.text.primary}}/>}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|
||||||
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
|
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Parent organization controlled environments. You can use, but not modify this environments. Contact an admin of your parent organization if you need changes to this."
|
title="Parent organization controlled environments. You can use, but not modify this environments. Contact an admin of your parent organization if you need changes to this."
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
label={"Parent"}
|
style={{marginLeft: 200, }}
|
||||||
variant="contained"
|
label={"Parent"}
|
||||||
color="secondary"
|
variant="contained"
|
||||||
/>
|
color="secondary"
|
||||||
</Tooltip>
|
/>
|
||||||
}
|
</Tooltip>
|
||||||
style={{ textAlign: 'center', verticalAlign: 'middle', }}
|
}
|
||||||
/>
|
style={{ textAlign: 'center', verticalAlign: 'middle', }}
|
||||||
:
|
/>
|
||||||
<Tooltip
|
:
|
||||||
title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."}
|
<Tooltip
|
||||||
placement="top"
|
title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."}
|
||||||
>
|
placement="top"
|
||||||
<IconButton
|
>
|
||||||
sx={{":hover": {backgroundColor: "transparent"}}}
|
<IconButton
|
||||||
|
style={{marginLeft: 200, }}
|
||||||
disabled={ environment.Name === "Cloud" || userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
|
disabled={ environment.Name === "Cloud" || userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -1489,35 +1538,41 @@ const EnvironmentTab = memo((props) => {
|
|||||||
aria-label="disabled tabs example"
|
aria-label="disabled tabs example"
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
style={{textAlign: "center", marginTop: 25, }}
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 25,
|
||||||
|
marginBottom: 25,
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.3)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tab
|
|
||||||
value={0}
|
|
||||||
label=<span style={{color: theme.palette.text.secondary, }}>
|
|
||||||
<img
|
|
||||||
src="/icons/docker.svg"
|
|
||||||
style={{ width: 20, height: 20, marginRight: 10, }}
|
|
||||||
/> Verbose (default)
|
|
||||||
</span>
|
|
||||||
/>
|
|
||||||
<Tab
|
<Tab
|
||||||
value={1}
|
value={1}
|
||||||
label=<span style={{color: theme.palette.text.secondary, }}>
|
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||||
<img
|
<img
|
||||||
src="/icons/docker.svg"
|
src="/icons/docker.svg"
|
||||||
style={{ width: 20, height: 20, marginRight: 10,}}
|
style={{ width: 20, height: 20, marginRight: 10,}}
|
||||||
/> Scale
|
/> Docker (default)
|
||||||
</span>
|
</span>
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
value={2}
|
value={2}
|
||||||
label=<span style={{color: theme.palette.text.secondary, }}>
|
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||||
<img
|
<img
|
||||||
src="/icons/k8s.svg"
|
src="/icons/k8s.svg"
|
||||||
style={{ width: 20, height: 20, marginRight: 10 }}
|
style={{ width: 20, height: 20, marginRight: 10 }}
|
||||||
/> k8s
|
/> Kubernetes
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
value={0}
|
||||||
|
style={{marginLeft: 300, }}
|
||||||
|
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||||
|
Verbose Mode
|
||||||
|
</span>
|
||||||
|
/>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||||
{installationTab === 2 ?
|
{installationTab === 2 ?
|
||||||
@@ -1563,6 +1618,7 @@ const EnvironmentTab = memo((props) => {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd",
|
border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd",
|
||||||
|
minHeight: 325,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getOrborusCommand(environment)}
|
{getOrborusCommand(environment)}
|
||||||
@@ -1588,49 +1644,114 @@ const EnvironmentTab = memo((props) => {
|
|||||||
|
|
||||||
<Divider style={{marginTop: 25, marginBottom: 10, }}/>
|
<Divider style={{marginTop: 25, marginBottom: 10, }}/>
|
||||||
<div style={{display: 'flex', alignItems: 'center', }}>
|
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||||
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies:</Typography> <Checkbox
|
<Checkbox
|
||||||
id="shuffle_skip_proxies"
|
id="shuffle_skip_proxies"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (commandController.proxies === undefined) {
|
if (commandController.proxies === undefined) {
|
||||||
commandController.proxies = true
|
commandController.proxies = true
|
||||||
} else {
|
} else {
|
||||||
commandController.proxies = !commandController.proxies
|
commandController.proxies = !commandController.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandController(commandController)
|
setCommandController(commandController)
|
||||||
setUpdate(Math.random())
|
setUpdate(Math.random())
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div />
|
<div />
|
||||||
<div style={{display: 'flex', alignItems: 'center', }}>
|
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||||
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake:</Typography> <Checkbox
|
<Checkbox
|
||||||
id="shuffle_skip_pipelines"
|
id="shuffle_enable_detection"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (commandController.pipelines === undefined) {
|
if (commandController.detection === undefined) {
|
||||||
commandController.pipelines = true
|
commandController.detection = true
|
||||||
} else {
|
} else {
|
||||||
commandController.pipelines = !commandController.pipelines
|
commandController.detection = !commandController.detection
|
||||||
}
|
}
|
||||||
setCommandController(commandController)
|
|
||||||
setUpdate(Math.random())
|
setCommandController(commandController)
|
||||||
}}
|
setUpdate(Math.random())
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||||
|
<Checkbox
|
||||||
|
id="shuffle_skip_pipelines"
|
||||||
|
onClick={() => {
|
||||||
|
if (commandController.pipelines === undefined) {
|
||||||
|
commandController.pipelines = true
|
||||||
|
} else {
|
||||||
|
commandController.pipelines = !commandController.pipelines
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommandController(commandController)
|
||||||
|
setUpdate(Math.random())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
|
||||||
|
</div>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||||
{installationTab === 2 ? null :
|
{installationTab === 2 ? null :
|
||||||
<span>
|
<span>
|
||||||
3. Verify if the node is running. Try to refresh the page a little while after running the command.
|
3. Verify if the Runtime Location is running. Refresh the page 2 minutes after running the command.
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
{currentEnvQueue.length === 0 ? null :
|
||||||
</Collapse>
|
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
|
||||||
|
{currentEnvQueue.map((queueItem, queueIndex) => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.surfaceColor,
|
||||||
|
borderBottom: theme.palette.defaultBorder,
|
||||||
|
maxHeight: 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
|
||||||
|
{queueItem.priority}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemText style={{minWidth: 125, maxWidth: 150, marginLeft: 25, }}>
|
||||||
|
{queueItem.type}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemText style={{minWidth: 325, maxWidth: 350, marginLeft: 25, overflow: "auto", }}>
|
||||||
|
{queueItem.execution_argument}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemText style={{minWidth: 50, maxWidth: 50, marginLeft: 25, overflow: "auto", }}>
|
||||||
|
<Tooltip title="Remove job from queue">
|
||||||
|
<IconButton
|
||||||
|
onClick={()=>{
|
||||||
|
removeEnvQueueItem(
|
||||||
|
environment,
|
||||||
|
queueItem,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{
|
||||||
|
color: red,
|
||||||
|
}} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
{showCPUAlert === false ? null : (
|
{showCPUAlert === false ? null : (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext, memo } from "react";
|
import React, { useState, useEffect, useContext, memo } from "react";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { GetIconInfo, } from "../views/Workflows2.jsx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -9,24 +10,25 @@ import {
|
|||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
ButtonGroup,
|
||||||
InputLabel,
|
FormControl,
|
||||||
TextField,
|
InputLabel,
|
||||||
Divider,
|
TextField,
|
||||||
Select,
|
Divider,
|
||||||
MenuItem,
|
Select,
|
||||||
Dialog,
|
MenuItem,
|
||||||
DialogTitle,
|
Dialog,
|
||||||
DialogContent,
|
DialogTitle,
|
||||||
DialogActions,
|
DialogContent,
|
||||||
Typography,
|
DialogActions,
|
||||||
Skeleton,
|
Typography,
|
||||||
Checkbox,
|
Skeleton,
|
||||||
Chip,
|
Checkbox,
|
||||||
Menu,
|
Chip,
|
||||||
Pagination,
|
Menu,
|
||||||
PaginationItem,
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { DataGrid } from "@mui/x-data-grid";
|
import { DataGrid } from "@mui/x-data-grid";
|
||||||
@@ -352,8 +354,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={"Delete file"}
|
title={"Delete file"}
|
||||||
style={{marginLeft: isSelectedFiles?5:15, }}
|
style={{}}
|
||||||
aria-label={"Delete"}
|
aria-label={"Delete"}
|
||||||
|
placement="right"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -628,8 +631,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folder === undefined || folder === null || folder.length < 2) {
|
if (folder === undefined || folder === null || folder.length < 1) {
|
||||||
toast("Please enter a valid folder name")
|
toast("Please enter a valid folder name. For Root: /")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +640,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
url: url,
|
url: url,
|
||||||
path: folder,
|
path: folder,
|
||||||
field_3: downloadBranch || "master",
|
field_3: downloadBranch || "master",
|
||||||
|
|
||||||
|
namespace: selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" ? selectedCategory : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (field1.length > 0) {
|
if (field1.length > 0) {
|
||||||
@@ -1312,57 +1317,80 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<ButtonGroup style={{top: -10, position: "relative", }}>
|
||||||
color="primary"
|
<Button
|
||||||
variant="contained"
|
color="primary"
|
||||||
onClick={() => {
|
variant="contained"
|
||||||
upload.click();
|
onClick={() => {
|
||||||
}}
|
upload.click();
|
||||||
style={{ textTransform: 'none',fontSize: 16, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
|
}}
|
||||||
>
|
style={{ textTransform: 'none',fontSize: 16, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
|
||||||
Upload files
|
>
|
||||||
</Button>
|
Upload files
|
||||||
{/* <FileCategoryInput
|
</Button>
|
||||||
isSet={renderTextBox} /> */}
|
{/* <FileCategoryInput
|
||||||
<input
|
isSet={renderTextBox} /> */}
|
||||||
hidden
|
<input
|
||||||
type="file"
|
hidden
|
||||||
multiple
|
type="file"
|
||||||
ref={(ref) => (upload = ref)}
|
multiple
|
||||||
onChange={(event) => {
|
ref={(ref) => (upload = ref)}
|
||||||
//const file = event.target.value
|
onChange={(event) => {
|
||||||
//const fileObject = URL.createObjectURL(actualFile)
|
//const file = event.target.value
|
||||||
//setFile(fileObject)
|
//const fileObject = URL.createObjectURL(actualFile)
|
||||||
//const files = event.target.files[0]
|
//setFile(fileObject)
|
||||||
uploadFiles(event.target.files);
|
//const files = event.target.files[0]
|
||||||
|
uploadFiles(event.target.files);
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
style={{ marginLeft: 16, marginRight: 15, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
|
style={{ width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => getFiles(selectedCategory)}
|
onClick={() => getFiles(selectedCategory)}
|
||||||
>
|
>
|
||||||
<CachedIcon />
|
<CachedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup style={{marginLeft: 10, }}>
|
||||||
{/* <div style={{height: 35, width: 1, color: "#494949"}}></div> */}
|
{/* <div style={{height: 35, width: 1, color: "#494949"}}></div> */}
|
||||||
|
|
||||||
|
{selectedCategory === "sigma" || selectedCategory === "yara" ?
|
||||||
|
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
|
||||||
|
<a href={`/detections/${selectedCategory}`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<OpenInNewIcon
|
||||||
|
color="primary"
|
||||||
|
style={{
|
||||||
|
marginLeft: 10,
|
||||||
|
marginRight: 10,
|
||||||
|
top: 7,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
: null}
|
||||||
|
|
||||||
{fileCategories !== undefined &&
|
{fileCategories !== undefined &&
|
||||||
fileCategories !== null &&
|
fileCategories !== null &&
|
||||||
fileCategories.length > 1 ? (
|
fileCategories.length > 1 ? (
|
||||||
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
|
<FormControl style={{ minWidth: 175, maxWidth: 175, }}>
|
||||||
|
<InputLabel id="category-choice" style={{
|
||||||
|
color: "rgba(255, 255, 255, 0.65)",
|
||||||
|
}}>
|
||||||
|
Category
|
||||||
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="input-namespace-select-label"
|
labelId="category-choice"
|
||||||
id="input-namespace-select-id"
|
id="input-namespace-select-id"
|
||||||
style={{
|
style={{
|
||||||
minWidth: 122,
|
minWidth: 175,
|
||||||
maxWidth: 122,
|
maxWidth: 175,
|
||||||
height: 35,
|
height: 35,
|
||||||
float: "right",
|
borderRadius: "5px 0px 0px 5px",
|
||||||
position: 'relative',
|
overflow: "hidden",
|
||||||
top: 8
|
|
||||||
}}
|
}}
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -1389,12 +1417,31 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fileCategories.map((data, index) => {
|
{fileCategories.map((data, index) => {
|
||||||
|
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||||
|
const iconDetails = GetIconInfo({
|
||||||
|
"app_name": fixedname,
|
||||||
|
"name": fixedname,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={index}
|
key={index}
|
||||||
value={data}
|
value={data}
|
||||||
|
style={{
|
||||||
|
color: theme.palette.textFieldStyle.color,
|
||||||
|
display: "flex",
|
||||||
|
borderBottom: theme.palette.defaultBorder,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{data.replaceAll("_", " ")}
|
<Typography style={{display: "flex", marginTop: 5, }}>
|
||||||
|
<div style={{marginRight: 10, }}>
|
||||||
|
{iconDetails?.originalIcon && (
|
||||||
|
iconDetails?.originalIcon
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fixedname}
|
||||||
|
</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -1430,36 +1477,51 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div style={{display: "inline-flex", position:"relative", top: 8}}>
|
|
||||||
{renderTextBox ?
|
{/*<div style={{display: "inline-flex", position:"relative", top: 8}}>*/}
|
||||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
{renderTextBox ?
|
||||||
<Button
|
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||||
style={{ marginLeft: 5, marginRight: 15, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
|
<Button
|
||||||
variant="contained"
|
style={{
|
||||||
color="secondary"
|
height: 35,
|
||||||
onClick={() => {
|
borderRadius: 4,
|
||||||
setRenderTextBox(false);
|
textTransform: 'none',
|
||||||
console.log(" close clicked")
|
fontSize: 16,
|
||||||
|
borderRadius: "0px 5px 5px 0px",
|
||||||
|
|
||||||
|
marginRight: 10,
|
||||||
}}
|
}}
|
||||||
>
|
color="secondary"
|
||||||
<ClearIcon/>
|
variant="contained"
|
||||||
</Button>
|
onClick={() => {
|
||||||
</Tooltip>
|
setRenderTextBox(false);
|
||||||
:
|
console.log(" close clicked")
|
||||||
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
|
|
||||||
<Button
|
|
||||||
style={{whiteSpace: 'nowrap', textWrap: 'nowrap', marginLeft: 5, marginRight: 15, width: 169, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setRenderTextBox(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddIcon/>
|
<ClearIcon/>
|
||||||
File Category
|
</Button>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</Tooltip>
|
:
|
||||||
}
|
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
width: fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? 50 : 169,
|
||||||
|
height: 35,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setRenderTextBox(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
{renderTextBox && <TextField
|
{renderTextBox && <TextField
|
||||||
onKeyPress={(event)=>{
|
onKeyPress={(event)=>{
|
||||||
@@ -1491,7 +1553,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
defaultValue={""}
|
defaultValue={""}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>}</div>
|
/>}
|
||||||
|
|
||||||
<ShuffleCodeEditor
|
<ShuffleCodeEditor
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
expansionModalOpen={openEditor}
|
expansionModalOpen={openEditor}
|
||||||
@@ -1673,3 +1736,4 @@ const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
region_url: org.region_url,
|
region_url: org.region_url,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
|
const [activeOrgData, setActiveOrgData] = useState(null);
|
||||||
|
const [isProdStatusOn, setIsProdStatusOn] = useState(false);
|
||||||
const userOrgs = React.useMemo(() => {
|
const userOrgs = React.useMemo(() => {
|
||||||
return orgOptions.find((option) => option.name === selectedOrg);
|
return orgOptions.find((option) => option.name === selectedOrg);
|
||||||
}, [selectedOrg, orgOptions]);
|
}, [selectedOrg, orgOptions]);
|
||||||
@@ -247,7 +249,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
|
|
||||||
const UpdateTabStatus = useCallback(() => {
|
const UpdateTabStatus = useCallback(() => {
|
||||||
const lastTabOpenByUser = localStorage.getItem("lastTabOpenByUser");
|
const lastTabOpenByUser = localStorage.getItem("lastTabOpenByUser");
|
||||||
if ((lastTabOpenByUser === "automate" && currentPath.includes("/dashboards/automate")) || currentPath.includes("/dashboards/automate")) {
|
if ((lastTabOpenByUser === "automate" && currentPath.includes("/new-dashboard")) || currentPath.includes("/new-dashboard")) {
|
||||||
setOpenautomateTab(true);
|
setOpenautomateTab(true);
|
||||||
setCurrentOpenTab("automate");
|
setCurrentOpenTab("automate");
|
||||||
setOpenSecurityTab(false);
|
setOpenSecurityTab(false);
|
||||||
@@ -682,7 +684,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
|
|
||||||
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
|
<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">
|
Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer">
|
||||||
2.1.0
|
2.1.1
|
||||||
</a>
|
</a>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -749,6 +751,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
localStorage.setItem("getting_started_sidebar", "open");
|
localStorage.setItem("getting_started_sidebar", "open");
|
||||||
localStorage.removeItem("workflows");
|
localStorage.removeItem("workflows");
|
||||||
localStorage.removeItem("apps");
|
localStorage.removeItem("apps");
|
||||||
|
localStorage.removeItem("dashboard_onboarding_complete")
|
||||||
|
localStorage.removeItem("dashboard_onboarding_completed")
|
||||||
|
|
||||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
@@ -936,6 +940,40 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
const showPartnerLogo = userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.image !== undefined && userdata?.active_org?.image !== null && userdata?.active_org?.image.length > 0
|
const showPartnerLogo = userdata?.org_status?.includes("integration_partner") && userdata?.active_org?.image !== undefined && userdata?.active_org?.image !== null && userdata?.active_org?.image.length > 0
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const orgId = userdata?.active_org?.id;
|
||||||
|
if (!orgId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetched = false;
|
||||||
|
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((response) => (response.ok ? response.json() : null))
|
||||||
|
.then((org) => {
|
||||||
|
if (!fetched && org) {
|
||||||
|
setActiveOrgData(org);
|
||||||
|
if (!isCloud) {
|
||||||
|
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||||
|
setIsProdStatusOn(true);
|
||||||
|
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||||
|
setIsProdStatusOn(true);
|
||||||
|
} else {
|
||||||
|
setIsProdStatusOn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
fetched = true;
|
||||||
|
};
|
||||||
|
}, [userdata?.active_org?.id, globalUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -975,6 +1013,14 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Go to Home"
|
title="Go to Home"
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -1005,7 +1051,15 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }}
|
style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{
|
||||||
|
!isCloud && expandLeftNav && (
|
||||||
|
<Typography variant="body2" style={{fontSize: 16, color: themeMode === "dark" ? lightText : darkText, transition: "opacity 0.3s ease", fontWeight: 600, marginTop: -5, marginLeft: 3}}>
|
||||||
|
{isProdStatusOn ? "Enterprise" : "Open Source"}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -1172,23 +1226,23 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
|
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/usecases"
|
to={"/new-dashboard"}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
setOpenautomateTab(true);
|
setOpenautomateTab(true);
|
||||||
setOpenSecurityTab(false);
|
setOpenSecurityTab(false);
|
||||||
setCurrentOpenTab("usecases");
|
setCurrentOpenTab("new-dashboard");
|
||||||
localStorage.setItem("lastTabOpenByUser", "usecases");
|
localStorage.setItem("lastTabOpenByUser", "new-dashboard");
|
||||||
}}
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
style={{
|
style={{
|
||||||
...ButtonStyle,
|
...ButtonStyle,
|
||||||
backgroundColor: ((currentOpenTab === "automate") || (!expandLeftNav && (currentPath === "/workflows" || currentPath === "/usecases" || currentPath.includes("/search"))))? themeMode === "dark" ? darkHoverColor : lightHoverColor : "transparent",
|
backgroundColor: ((currentOpenTab === "new-dashboard") || (!expandLeftNav && (currentPath === "/workflows" || currentPath === "/usecases" || currentPath.includes("/search"))))? themeMode === "dark" ? darkHoverColor : lightHoverColor : "transparent",
|
||||||
}}
|
}}
|
||||||
onMouseOver={(event)=>{
|
onMouseOver={(event)=>{
|
||||||
event.currentTarget.style.backgroundColor = themeMode === "dark" ? darkHoverColor : lightHoverColor;
|
event.currentTarget.style.backgroundColor = themeMode === "dark" ? darkHoverColor : lightHoverColor;
|
||||||
}}
|
}}
|
||||||
onMouseOut={(event)=>{
|
onMouseOut={(event)=>{
|
||||||
event.currentTarget.style.backgroundColor = ((currentOpenTab === "automate")|| (!expandLeftNav && (currentPath === "/workflows" || currentPath === "/usecases" || currentPath.includes("/search"))))? themeMode === "dark" ? darkHoverColor : lightHoverColor : "transparent";
|
event.currentTarget.style.backgroundColor = ((currentOpenTab === "new-dashboard")|| (!expandLeftNav && (currentPath === "/workflows" || currentPath === "/usecases" || currentPath.includes("/search"))))? themeMode === "dark" ? darkHoverColor : lightHoverColor : "transparent";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -1871,19 +1925,68 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav &&
|
|
||||||
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
|
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn &&
|
||||||
<Button
|
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
|
||||||
variant="outlined"
|
<Button
|
||||||
style={{marginBottom: 15, borderWidth: 2, }}
|
variant="outlined"
|
||||||
onClick={() => {
|
style={{marginBottom: 15, borderWidth: 2, }}
|
||||||
window.open("https://shuffler.io/contact?category=book_a_demo", "_blank")
|
onClick={() => {
|
||||||
}}
|
navigate("/admin?admin_tab=billingstats&ref=left_sidebar_upgrade")
|
||||||
>
|
if (isCloud) {
|
||||||
Book a Demo
|
window.open("https://shuffler.io/contact?category=book_a_demo&ref=cloud", "_blank")
|
||||||
</Button>
|
} else {
|
||||||
</div>
|
window.open("https://shuffler.io/contact?category=book_a_demo&ref=onprem", "_blank")
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Book a Demo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{!isCloud ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 20,
|
||||||
|
padding: "4px 10px",
|
||||||
|
marginLeft: "5px",
|
||||||
|
marginRight: "5px",
|
||||||
|
borderRadius: 20,
|
||||||
|
marginBottom: "14px",
|
||||||
|
background: isProdStatusOn
|
||||||
|
? "rgba(43, 192, 126, 0.1)"
|
||||||
|
: "rgba(255, 82, 82, 0.1)",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/admin?admin_tab=billingstats")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
marginLeft: 10,
|
||||||
|
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||||
|
borderRadius: 999,
|
||||||
|
display: expandLeftNav ? "inline" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
style={{
|
||||||
|
fontFamily: "12px",
|
||||||
|
opacity: 0.9,
|
||||||
|
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Box ref={autocompleteRef}>
|
<Box ref={autocompleteRef}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
|
|||||||
+1561
-1569
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useContext, memo, useMemo } from 'react'
|
import React, { useState, useEffect, useContext, memo, useMemo } from 'react'
|
||||||
import {getTheme} from '../theme.jsx';
|
import {getTheme} from '../theme.jsx';
|
||||||
import { Context } from '../context/ContextApi.jsx';
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
@@ -14,19 +15,104 @@ import {
|
|||||||
|
|
||||||
GridlineSeries,
|
GridlineSeries,
|
||||||
Gridline,
|
Gridline,
|
||||||
TooltipArea,
|
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
TooltipTemplate,
|
TooltipTemplate,
|
||||||
|
TooltipArea,
|
||||||
} from 'reaviz';
|
} from 'reaviz';
|
||||||
|
|
||||||
|
export const LoadStats = (globalUrl, cachekey) => {
|
||||||
|
if (globalUrl === undefined) {
|
||||||
|
console.log("Error: Global URL is undefined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachekey === undefined) {
|
||||||
|
console.log("Error: Cachekey is undefined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var basedata = {
|
||||||
|
"key": cachekey,
|
||||||
|
"total": 0,
|
||||||
|
"available_keys": [],
|
||||||
|
"labels": [],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"label": "",
|
||||||
|
"data": [],
|
||||||
|
"backgroundColor": [],
|
||||||
|
"barThickness": 15,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||||
|
//cachekey = cachekey.replace(" ", "_", -1)
|
||||||
|
const url = `${globalUrl}/api/v1/stats/${cachekey}`
|
||||||
|
return fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
return resp.json()
|
||||||
|
}).then((respJson) => {
|
||||||
|
const selectedIndex = 0
|
||||||
|
|
||||||
|
//console.log("Stats response: ", respJson)
|
||||||
|
|
||||||
|
if (respJson.success === true) {
|
||||||
|
for (let entryKey in respJson.entries) {
|
||||||
|
const entry = respJson.entries[entryKey]
|
||||||
|
basedata.labels.push(entry.date)
|
||||||
|
|
||||||
|
basedata.datasets[0].data.push(entry.value)
|
||||||
|
basedata.datasets[0].backgroundColor.push(entry.value > 0 ? "rgba(255,255,255,0.4)" : "red")
|
||||||
|
}
|
||||||
|
|
||||||
|
basedata.available_keys = respJson.available_keys
|
||||||
|
basedata.total = respJson.total
|
||||||
|
|
||||||
|
return basedata
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get stats")
|
||||||
|
return basedata
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast("Failed to get stats")
|
||||||
|
return basedata
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const LineChartWrapper = (props) => {
|
const LineChartWrapper = (props) => {
|
||||||
const {keys, inputname, height, width, border} = props
|
const {keys, inputname, height, width, border, color} = props
|
||||||
|
|
||||||
const [hovered, setHovered] = useState("");
|
const [hovered, setHovered] = useState("");
|
||||||
const {themeMode} = useContext(Context)
|
const {themeMode} = useContext(Context)
|
||||||
const theme = getTheme(themeMode)
|
const theme = getTheme(themeMode)
|
||||||
|
|
||||||
var inputdata = keys.data === undefined ? keys : keys.data
|
// Correct format:
|
||||||
|
/* keys={[
|
||||||
|
{
|
||||||
|
key: "2025-07-23T00:00:09.409718Z",
|
||||||
|
data: 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2025-07-24T00:00:09.409718Z",
|
||||||
|
data: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2025-07-25T00:00:09.409718Z",
|
||||||
|
data: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2025-07-26T00:00:09.409718Z",
|
||||||
|
data: 42,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var inputdata = keys?.data === undefined ? keys : keys.data
|
||||||
|
|
||||||
var newname = inputname === undefined || inputname === null ? "" : inputname.trim().replaceAll("_", " ")
|
var newname = inputname === undefined || inputname === null ? "" : inputname.trim().replaceAll("_", " ")
|
||||||
newname = newname.charAt(0).toUpperCase() + newname.slice(1)
|
newname = newname.charAt(0).toUpperCase() + newname.slice(1)
|
||||||
@@ -35,7 +121,6 @@ const LineChartWrapper = (props) => {
|
|||||||
|
|
||||||
var tmpdata = inputdata?.datasets[0]
|
var tmpdata = inputdata?.datasets[0]
|
||||||
if (tmpdata?.data !== undefined && tmpdata?.data !== null && tmpdata?.data.length > 0 && inputdata?.labels?.length === tmpdata?.data?.length) {
|
if (tmpdata?.data !== undefined && tmpdata?.data !== null && tmpdata?.data.length > 0 && inputdata?.labels?.length === tmpdata?.data?.length) {
|
||||||
console.log("Fix it!")
|
|
||||||
var newarray = []
|
var newarray = []
|
||||||
for (var key in tmpdata.data) {
|
for (var key in tmpdata.data) {
|
||||||
var entry = {
|
var entry = {
|
||||||
@@ -48,26 +133,25 @@ const LineChartWrapper = (props) => {
|
|||||||
|
|
||||||
inputdata = newarray
|
inputdata = newarray
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputdata === undefined || inputdata === null) {
|
if (inputdata === undefined || inputdata === null) {
|
||||||
return (
|
return null /*(
|
||||||
<Typography>
|
<Typography>
|
||||||
Invalid linegraph data format
|
Invalid linegraph data format
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultStyle = {
|
var defaultStyle = {
|
||||||
color: "white",
|
color: "white",
|
||||||
padding: 30,
|
padding: "5px 5px 10px 5px",
|
||||||
marginTop: 15,
|
marginTop: 15,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|
||||||
border: "1px solid rgba(255,255,255,0.3)",
|
border: "1px solid rgba(255,255,255,0.3)",
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
backgroundColor: theme.palette.platformColor,
|
//backgroundColor: theme.palette.platformColor,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (border === false) {
|
if (border === false) {
|
||||||
@@ -76,11 +160,64 @@ const LineChartWrapper = (props) => {
|
|||||||
defaultStyle.backgroundColor = "transparent"
|
defaultStyle.backgroundColor = "transparent"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a list or not
|
||||||
|
if (!Array.isArray(inputdata) || inputdata.length === 0 || (inputdata.length > 0 && (inputdata[0].key === undefined && inputdata[0].data === undefined))) {
|
||||||
|
console.log("Invalid graph data format: ", inputdata)
|
||||||
|
console.log("Expected format: [{key: 'label1', data: 10}, {key: 'label2', data: 20}]")
|
||||||
|
//inputdata = inputdata?.datasets[0]?.data
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("FORMAT: ", inputdata)
|
||||||
|
|
||||||
|
const tooltip = <TooltipArea
|
||||||
|
tooltip={
|
||||||
|
<ChartTooltip
|
||||||
|
placement={"top"}
|
||||||
|
followCursor={true}
|
||||||
|
content={(data) => (
|
||||||
|
<div style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
border: theme.palette.defaultBorder,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 6,
|
||||||
|
maxWidth: 240,
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" style={{fontWeight: 600}}>{data?.x ?? ''}</Typography>
|
||||||
|
<Typography variant="body2">{data?.y ?? ''}</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const selectedColor = color === undefined || color === null || color === "" ? "" : color
|
||||||
|
const barseries = color === undefined || color === null || color === "" ?
|
||||||
|
<BarSeries
|
||||||
|
bar={
|
||||||
|
<Bar
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
tooltip={tooltip}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<BarSeries
|
||||||
|
colorScheme={[selectedColor]}
|
||||||
|
bar={
|
||||||
|
<Bar
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
tooltip={tooltip}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={defaultStyle}>
|
<div style={defaultStyle}>
|
||||||
<Typography variant="h6" style={{marginBotton: 30, }}>
|
{newname !== "" &&
|
||||||
{newname}
|
<Typography variant="h6" style={{paddingBottom: 25, }}>
|
||||||
</Typography>
|
{newname}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
|
||||||
<BarChart
|
<BarChart
|
||||||
style={{marginTop: 100, }}
|
style={{marginTop: 100, }}
|
||||||
@@ -89,11 +226,8 @@ const LineChartWrapper = (props) => {
|
|||||||
data={inputdata}
|
data={inputdata}
|
||||||
|
|
||||||
series={
|
series={
|
||||||
<BarSeries
|
barseries
|
||||||
bar={
|
|
||||||
<Bar />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
gridlines={
|
gridlines={
|
||||||
<GridlineSeries line={<Gridline direction="all" />} />
|
<GridlineSeries line={<Gridline direction="all" />} />
|
||||||
|
|||||||
@@ -688,51 +688,51 @@ const AuthenticationOauth2 = (props) => {
|
|||||||
|
|
||||||
const autoAuthButton =
|
const autoAuthButton =
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
color: "#2f2f2f",
|
color: "#2f2f2f",
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
minWidth: 300,
|
minWidth: 275,
|
||||||
maxWidth: 300,
|
maxWidth: 275,
|
||||||
maxHeight: 50,
|
maxHeight: 50,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
border: `1px solid ${theme.palette.inputColor}`,
|
border: `1px solid ${theme.palette.inputColor}`,
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={
|
disabled={
|
||||||
clientSecret.length > 0 || clientId.length > 0
|
clientSecret.length > 0 || clientId.length > 0
|
||||||
}
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Hardcode some stuff?
|
// Hardcode some stuff?
|
||||||
// This could prolly be added to the app itself with a "default" client ID
|
// This could prolly be added to the app itself with a "default" client ID
|
||||||
startOauth2Request()
|
startOauth2Request()
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{buttonClicked ? (
|
{buttonClicked ? (
|
||||||
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
||||||
) : (
|
) : (
|
||||||
<span style={{display: "flex"}}>
|
<span style={{display: "flex"}}>
|
||||||
<img
|
<img
|
||||||
alt={selectedAction.app_name}
|
alt={selectedAction.app_name}
|
||||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||||
src={selectedAction.large_image}
|
src={selectedAction.large_image}
|
||||||
/>
|
/>
|
||||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5, color: "#2f2f2f",}} variant="body1">
|
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||||
One-click Login
|
One-click Login
|
||||||
</Typography>
|
</Typography>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
|
if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
|
||||||
return autoAuthButton
|
return autoAuthButton
|
||||||
@@ -747,7 +747,8 @@ const AuthenticationOauth2 = (props) => {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<span style={{}}>
|
<span style={{}}>
|
||||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b> - </span>
|
Oauth2 requires a Client ID and Client Secret to authenticate, defined in your apps' remote website. <span>Your redirect URL: <br/><b>{window.location.origin}/set_authentication</b><br/>
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="norefferer"
|
rel="norefferer"
|
||||||
@@ -760,51 +761,52 @@ const AuthenticationOauth2 = (props) => {
|
|||||||
<div />
|
<div />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
|
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
|
||||||
<span>
|
<span>
|
||||||
<span style={{display: "flex"}}>
|
<span style={{display: "flex"}}>
|
||||||
{autoAuthButton}
|
{autoAuthButton}
|
||||||
|
|
||||||
{buttonClicked ?
|
{buttonClicked ?
|
||||||
null
|
null
|
||||||
:
|
:
|
||||||
<Tooltip
|
<Tooltip
|
||||||
color="primary"
|
color="primary"
|
||||||
title={"Force Admin Consent"}
|
title={"Force Admin Consent"}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 50,
|
maxWidth: 40,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
maxHeight: 50,
|
maxHeight: 50,
|
||||||
}}
|
marginLeft: 10,
|
||||||
color="primary"
|
}}
|
||||||
disabled={
|
color="secondary"
|
||||||
clientSecret.length > 0 || clientId.length > 0
|
disabled={
|
||||||
}
|
clientSecret.length > 0 || clientId.length > 0
|
||||||
fullWidth
|
}
|
||||||
onClick={() => {
|
fullWidth
|
||||||
// Hardcode some stuff?
|
onClick={() => {
|
||||||
// This could prolly be added to the app itself with a "default" client ID
|
// Hardcode some stuff?
|
||||||
//startOauth2Request(true)
|
// This could prolly be added to the app itself with a "default" client ID
|
||||||
startOauth2Request()
|
//startOauth2Request(true)
|
||||||
}}
|
startOauth2Request()
|
||||||
color="primary"
|
}}
|
||||||
>
|
color="primary"
|
||||||
<SupervisorAccountIcon />
|
>
|
||||||
</Button>
|
<SupervisorAccountIcon />
|
||||||
</Tooltip>
|
</Button>
|
||||||
}
|
</Tooltip>
|
||||||
</span>
|
}
|
||||||
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
|
</span>
|
||||||
OR
|
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
|
||||||
</Typography>
|
OR
|
||||||
</span>
|
</Typography>
|
||||||
: null}
|
</span>
|
||||||
|
: null}
|
||||||
{/*<TextField
|
{/*<TextField
|
||||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
|
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
{userdata?.support ? (
|
{userdata?.support ? (
|
||||||
<div style={{ alignItems: 'center' }}>
|
<div style={{ alignItems: 'center' }}>
|
||||||
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}>Status</div>
|
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5 }}>Status</div>
|
||||||
<FormControl style={{ width: 220, height: 35 }}>
|
<FormControl style={{ width: 220, height: 35 }}>
|
||||||
<Select
|
<Select
|
||||||
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
|
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
|
||||||
@@ -711,11 +711,12 @@ const OrgHeaderexpandedNew = (props) => {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null :
|
||||||
<CloudSyncTab
|
<CloudSyncTab
|
||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
userdata={userdata}
|
userdata={userdata}
|
||||||
serverside={false}
|
serverside={false}
|
||||||
/>
|
/>}
|
||||||
<Grid item xs={12} style={{ marginTop: 20, }}>
|
<Grid item xs={12} style={{ marginTop: 20, }}>
|
||||||
<Typography variant="h5" style={{ textAlign: "left", fontWeight: 500, }}>Workflow Backup Repository</Typography>
|
<Typography variant="h5" style={{ textAlign: "left", fontWeight: 500, }}>Workflow Backup Repository</Typography>
|
||||||
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400 }}>
|
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400 }}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
||||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import Billing from "../components/Billing.jsx";
|
import Billing from "../components/Billing.jsx";
|
||||||
import Priorities from "../components/Priorities.jsx";
|
import Priorities from "../components/Priorities.jsx";
|
||||||
import Branding from "../components/Branding.jsx";
|
import Branding from "../components/Branding.jsx";
|
||||||
@@ -7,9 +8,20 @@ import EditOrgTab from '../components/EditOrgTab.jsx';
|
|||||||
import CloudSyncTab from '../components/CloudSyncTab.jsx';
|
import CloudSyncTab from '../components/CloudSyncTab.jsx';
|
||||||
import SSOTab from "../components/ssoTab.jsx"
|
import SSOTab from "../components/ssoTab.jsx"
|
||||||
import { ToastContainer, toast } from "react-toastify";
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
import { Button, Tooltip } from '@mui/material';
|
import {
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
import { getTheme } from '../theme.jsx';
|
import { getTheme } from '../theme.jsx';
|
||||||
import { Context } from '../context/ContextApi.jsx';
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
|
||||||
const OrganizationTab = (props) => {
|
const OrganizationTab = (props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -49,14 +61,18 @@ const OrganizationTab = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
||||||
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
|
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
|
||||||
}else {
|
} else {
|
||||||
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
||||||
setVisibleTabs(items);
|
setVisibleTabs(items);
|
||||||
}else {
|
} else {
|
||||||
setVisibleTabs(items.filter((item) => item !== 'SSO'));
|
setVisibleTabs(items.filter((item) => item !== 'SSO'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},[isIntegrationPartner, isChildOrg, isGlobalUser, userdata]);
|
|
||||||
|
//if (isCloud) {
|
||||||
|
// setVisibleTabs(items.filter((item) => item !== 'Production Status'));
|
||||||
|
//}
|
||||||
|
},[isIntegrationPartner, isChildOrg, isGlobalUser, userdata, isCloud]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
@@ -66,30 +82,32 @@ const OrganizationTab = (props) => {
|
|||||||
setSelectedTab(decodedTabName);
|
setSelectedTab(decodedTabName);
|
||||||
if (decodedTabName === 'org_config') {
|
if (decodedTabName === 'org_config') {
|
||||||
setCurIndex(0);
|
setCurIndex(0);
|
||||||
|
} else if (decodedTabName === 'prodstatus' || decodedTabName === 'productionstatus' && !isCloud) {
|
||||||
|
setCurIndex(1);
|
||||||
} else if(decodedTabName === 'sso'){
|
} else if(decodedTabName === 'sso'){
|
||||||
setCurIndex(1)
|
setCurIndex(isCloud ? 1 : 2)
|
||||||
}else if (decodedTabName === 'notifications' || decodedTabName === 'priorities') {
|
}else if (decodedTabName === 'notifications' || decodedTabName === 'priorities') {
|
||||||
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
||||||
setCurIndex(1);
|
setCurIndex(isCloud ? 1 : 2);
|
||||||
}else {
|
}else {
|
||||||
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
||||||
setCurIndex(2);
|
setCurIndex(isCloud ? 2 : 3);
|
||||||
}else {
|
}else {
|
||||||
setCurIndex(1);
|
setCurIndex(isCloud ? 1 : 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (decodedTabName === 'billingstats' || decodedTabName === 'billing') {
|
} else if (decodedTabName === 'billingstats' || decodedTabName === 'billing') {
|
||||||
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
||||||
setCurIndex(2);
|
setCurIndex(isCloud ? 2 : 3);
|
||||||
} else {
|
} else {
|
||||||
if (userdata && userdata?.active_org && userdata?.active_org?.role === 'admin') {
|
if (userdata && userdata?.active_org && userdata?.active_org?.role === 'admin') {
|
||||||
setCurIndex(3);
|
setCurIndex(isCloud ? 3 : 4);
|
||||||
} else {
|
} else {
|
||||||
setCurIndex(2);
|
setCurIndex(isCloud ? 2 : 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (decodedTabName === 'branding') {
|
} else if (decodedTabName === 'branding') {
|
||||||
setCurIndex(4);
|
setCurIndex(isCloud ? 4 : 5);
|
||||||
}
|
}
|
||||||
// else if (decodedTabName === 'analytics') {
|
// else if (decodedTabName === 'analytics') {
|
||||||
// setCurIndex(5);
|
// setCurIndex(5);
|
||||||
@@ -123,6 +141,7 @@ const OrganizationTab = (props) => {
|
|||||||
case 'sso':
|
case 'sso':
|
||||||
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
|
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
|
||||||
case `notifications`:
|
case `notifications`:
|
||||||
|
case `errors`:
|
||||||
case `priorities`:
|
case `priorities`:
|
||||||
return (
|
return (
|
||||||
<Priorities
|
<Priorities
|
||||||
@@ -183,7 +202,7 @@ const OrganizationTab = (props) => {
|
|||||||
key={index}
|
key={index}
|
||||||
title={
|
title={
|
||||||
((tabName === "sso")) && !(userdata?.support || userdata?.active_org?.role === "admin")
|
((tabName === "sso")) && !(userdata?.support || userdata?.active_org?.role === "admin")
|
||||||
? "Your role is not admin. Please ask the admin to change your role."
|
? "Your role is not admin. Please ask an admin to change your role."
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -248,7 +267,7 @@ const OrganizationTab = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', width: "100%", height: "100%", boxSizing:'border-box'}}>
|
<div style={{ display: 'flex', justifyContent: 'flex-start', width: "100%", height: "100%", boxSizing:'border-box'}}>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,3 +275,4 @@ const OrganizationTab = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default OrganizationTab;
|
export default OrganizationTab;
|
||||||
|
|
||||||
|
|||||||
@@ -437,7 +437,8 @@ const ParsedAction = (props) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const getApp = (appId, setApp) => {
|
const getApp = (appId, setApp) => {
|
||||||
fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", {
|
const url = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false`;
|
||||||
|
fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
@@ -447,7 +448,7 @@ const ParsedAction = (props) => {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
//toast("Successfully GOT app "+appId)
|
//toast("Successfully GOT app "+appId)
|
||||||
} else {
|
} else {
|
||||||
toast("Failed getting app");
|
toast.error("Failed getting app. Please try again or contact support@shuffler.io");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -1711,6 +1712,7 @@ const ParsedAction = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sortByCategoryLabel = (a, b) => {
|
const sortByCategoryLabel = (a, b) => {
|
||||||
|
|
||||||
const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
|
const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
|
||||||
const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
|
const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
|
||||||
|
|
||||||
@@ -1739,11 +1741,12 @@ const ParsedAction = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Gets the most important actions first
|
// Gets the most important actions first
|
||||||
const renderedActionOptions = deduplicateByName((
|
const renderedActionOptions = deduplicateByName((
|
||||||
selectedApp.actions === undefined || selectedApp.actions === null ? [] :
|
selectedApp.actions === undefined || selectedApp.actions === null ? [] :
|
||||||
selectedApp.actions.filter((a) =>
|
isIntegration ? selectedApp.actions :
|
||||||
a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
|
selectedApp.actions.filter((a) => a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
|
||||||
).sort(sortByCategoryLabel))
|
).sort(sortByCategoryLabel))
|
||||||
|
|
||||||
|
|
||||||
@@ -2981,7 +2984,6 @@ const ParsedAction = (props) => {
|
|||||||
dataLPIgnore="true"
|
dataLPIgnore="true"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
||||||
|
|
||||||
id="checkbox-search"
|
id="checkbox-search"
|
||||||
style={{
|
style={{
|
||||||
...theme.palette.textFieldStyle,
|
...theme.palette.textFieldStyle,
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ const PartnerDetails = (props) => {
|
|||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
|
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
|
||||||
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
|
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
|
||||||
Name
|
Company Name
|
||||||
</Typography>
|
</Typography>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div style={{ alignItems: "center" }}>
|
<div style={{ alignItems: "center" }}>
|
||||||
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
|
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5}}>
|
||||||
Solutions
|
Solutions
|
||||||
</div>
|
</div>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
@@ -399,7 +399,7 @@ const PartnerDetails = (props) => {
|
|||||||
variant="text"
|
variant="text"
|
||||||
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
|
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
|
||||||
>
|
>
|
||||||
Name
|
Company Name
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -419,7 +419,7 @@ const PartnerDetails = (props) => {
|
|||||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||||
}}
|
}}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
placeholder="Name"
|
placeholder="Company Name"
|
||||||
type="name"
|
type="name"
|
||||||
id="standard-required"
|
id="standard-required"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
@@ -544,7 +544,8 @@ const PartnerDetails = (props) => {
|
|||||||
style={{
|
style={{
|
||||||
marginRight: "12px",
|
marginRight: "12px",
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
fontFamily: theme?.typography?.fontFamily
|
fontFamily: theme?.typography?.fontFamily,
|
||||||
|
marginTop: 2.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Solutions
|
Solutions
|
||||||
@@ -895,7 +896,7 @@ const PartnerDetails = (props) => {
|
|||||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||||
}}
|
}}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
placeholder="support@shuffler.io"
|
placeholder="example@company.com"
|
||||||
type="name"
|
type="name"
|
||||||
id="standard-required"
|
id="standard-required"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
|
|||||||
@@ -1362,10 +1362,10 @@ print('"' + encoded + '"')
|
|||||||
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
||||||
<div style={{ maxWidth: "calc(100% - 20px)" }}>
|
<div style={{ maxWidth: "calc(100% - 20px)" }}>
|
||||||
<Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}>
|
<Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}>
|
||||||
Notification Workflow
|
Error Workflow
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="textSecondary" style={{ fontSize: 16, fontWeight: 400, marginTop: 5, }}>
|
<Typography color="textSecondary" style={{ fontSize: 16, fontWeight: 400, marginTop: 5, }}>
|
||||||
The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org notifications into the parent org notification by choosing it in the list.</b>
|
The error workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org errors to a parent org's error workflow by choosing it in the list.</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{modalView}
|
{modalView}
|
||||||
@@ -1518,6 +1518,7 @@ print('"' + encoded + '"')
|
|||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<TextField
|
<TextField
|
||||||
|
disabled={workflows === undefined || workflows === null || workflows.length === 0}
|
||||||
required
|
required
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: {
|
style: {
|
||||||
@@ -1614,12 +1615,12 @@ print('"' + encoded + '"')
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Notifications ({
|
<Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Errors ({
|
||||||
notifications?.filter((notification) => showRead === true || notification.read === false).length
|
notifications?.filter((notification) => showRead === true || notification.read === false).length
|
||||||
})</Typography>
|
})</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16, marginLeft: clickedFromOrgTab ? null : 25, color: clickedFromOrgTab ? "#9E9E9E" : null, }}>
|
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16, marginLeft: clickedFromOrgTab ? null : 25, color: clickedFromOrgTab ? "#9E9E9E" : null, }}>
|
||||||
Notifications help you find potential problems with your workflows and apps.
|
Error help you find potential problems with your workflows and apps.
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ const Priority = (props) => {
|
|||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
descsplit[1] = item.large_image
|
descsplit[1] = item.large_image
|
||||||
|
|
||||||
console.log("DESCSPLIT name: ", descsplit[0])
|
|
||||||
|
|
||||||
if (descsplit[0].includes(":default")) {
|
if (descsplit[0].includes(":default")) {
|
||||||
realignedSrc = true
|
realignedSrc = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
CircularProgress,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Check as CheckIcon,
|
||||||
|
OpenInNew as OpenInNewIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
|
||||||
|
|
||||||
|
const RunDetectionTest = (props) => {
|
||||||
|
const {
|
||||||
|
globalUrl,
|
||||||
|
pipelines,
|
||||||
|
workflows,
|
||||||
|
ticketWebhook,
|
||||||
|
detectionWorkflowId,
|
||||||
|
|
||||||
|
changePipelineState,
|
||||||
|
submitPipelineWrapper,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [executions, setExecutions] = React.useState([]);
|
||||||
|
const [detectionTestRunning, setDetectionTestRunning] = React.useState(false);
|
||||||
|
const [detectionTestExecutionId, setDetectionTestExecutionId] = React.useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (detectionWorkflowId !== "") {
|
||||||
|
handleLoadExecutions(detectionWorkflowId)
|
||||||
|
}
|
||||||
|
}, [detectionWorkflowId])
|
||||||
|
|
||||||
|
if (workflows === undefined || workflows === null || workflows.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoadExecutions = (workflowId, detectionTestRunning) => {
|
||||||
|
const url = `${globalUrl}/api/v2/workflows/${workflowId}/executions`
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for getting all executions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson.success !== false && responseJson?.executions?.length > 0) {
|
||||||
|
if (detectionTestRunning === true) {
|
||||||
|
console.log("Checking executions in workflow: ", workflowId, responseJson.executions)
|
||||||
|
for (var executionKey in responseJson.executions) {
|
||||||
|
const curExec = responseJson.executions[executionKey]
|
||||||
|
|
||||||
|
if (curExec.execution_id === detectionTestExecutionId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// started_at = unix timestamp
|
||||||
|
// check within the last 60 seconds
|
||||||
|
const datecomparison = (Date.now() / 1000) - 60
|
||||||
|
if (curExec.started_at >= datecomparison) {
|
||||||
|
if (curExec?.execution_argument?.includes("rule") && curExec?.execution_argument?.includes("Test Notepad Event")) {
|
||||||
|
setDetectionTestRunning(false)
|
||||||
|
setDetectionTestExecutionId(curExec.execution_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setExecutions(responseJson.executions || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const runDetectionTest = () => {
|
||||||
|
setDetectionTestRunning(true)
|
||||||
|
if (ticketWebhook === "") {
|
||||||
|
setDetectionTestRunning(false)
|
||||||
|
toast.error("No ticketing webhook found. Please enable the ticketing workflow first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectionWorkflowId === "") {
|
||||||
|
setDetectionTestRunning(false)
|
||||||
|
toast.error("No ticketing workflow found. Please enable the ticketing workflow first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haveDetectionPipelines() === false) {
|
||||||
|
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`
|
||||||
|
|
||||||
|
for (var pipelineKey in pipelines) {
|
||||||
|
const curPipeline = pipelines[pipelineKey]
|
||||||
|
if (curPipeline.definition === detectionTest && changePipelineState !== undefined) {
|
||||||
|
changePipelineState(curPipeline, "stop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Submit it to run
|
||||||
|
// 2. Check executions if they happened recently~
|
||||||
|
if (submitPipelineWrapper !== undefined) {
|
||||||
|
submitPipelineWrapper(detectionTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleLoadExecutions(detectionWorkflowId, true)
|
||||||
|
}, i * 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setDetectionTestRunning(false)
|
||||||
|
}, 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const haveDetectionPipelines = () => {
|
||||||
|
if (pipelines === undefined) {
|
||||||
|
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundCorrect = 0
|
||||||
|
for (var pipelineKey in pipelines) {
|
||||||
|
const curPipeline = pipelines[pipelineKey]
|
||||||
|
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
|
||||||
|
// foundCorrect += 1
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
|
||||||
|
foundCorrect += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundCorrect >= 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
|
||||||
|
onClick={() => {
|
||||||
|
//setPipelineModalOpen(true)
|
||||||
|
runDetectionTest()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
detectionTestRunning === true ? <CircularProgress size={20} style={{marginRight: 10, }} /> : "Run Detection Test"
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{detectionTestRunning === false && detectionTestExecutionId !== "" && detectionTestExecutionId !== undefined ?
|
||||||
|
<Tooltip title={`Go to detection test: ${detectionTestExecutionId}`} style={{}} aria-label={"Go to logs"}>
|
||||||
|
<a href={`/workflows/${detectionWorkflowId}?execution_id=${detectionTestExecutionId}`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
minWidth: 75, maxWidth: 75,
|
||||||
|
minHeight: 40, maxHeight: 40,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon style={{color: green}}/>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
: null}
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
{window?.location?.href?.includes("/detections/") === true ? null :
|
||||||
|
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
|
||||||
|
<a href={`/detections/sigma`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<OpenInNewIcon
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
marginLeft: 20,
|
||||||
|
top: 8,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RunDetectionTest
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Typography, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material';
|
||||||
|
import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
|
||||||
|
import theme from '../theme.jsx';
|
||||||
|
|
||||||
|
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
|
||||||
|
function formatCompactNumber(value) {
|
||||||
|
const n = Number(value) || 0;
|
||||||
|
const abs = Math.abs(n);
|
||||||
|
if (abs >= 1e9) return `${Math.round((n / 1e9) * 10) / 10}B`;
|
||||||
|
if (abs >= 1e6) return `${Math.round((n / 1e6) * 10) / 10}M`;
|
||||||
|
if (abs >= 1e3) return `${Math.round((n / 1e3) * 10) / 10}k`;
|
||||||
|
return `${n}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunsOverTimeWidget = (props) => {
|
||||||
|
const { globalUrl, onLoadingChange, monthOverride, dummyMode } = props;
|
||||||
|
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
||||||
|
const [series, setSeries] = useState([]);
|
||||||
|
const [days, setDays] = useState(365); // aggregate to last 12 months by default
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Helper: fetch time series for a specific statistics key
|
||||||
|
const fetchSeriesForKey = async (key) => {
|
||||||
|
try {
|
||||||
|
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' } });
|
||||||
|
if (!r.ok) return [];
|
||||||
|
const j = await r.json();
|
||||||
|
return Array.isArray(j?.entries) ? j.entries : [];
|
||||||
|
};
|
||||||
|
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 [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load and transform into monthly aggregation for last 12 months
|
||||||
|
const load = async (curMode) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Clear current series immediately to avoid any visual overlap while switching views
|
||||||
|
setSeries([]);
|
||||||
|
if (dummyMode) {
|
||||||
|
// Bring back the older dummy series with emphasis on earlier months
|
||||||
|
const now = new Date();
|
||||||
|
const months = [];
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
months.push(new Date(dt.getFullYear(), dt.getMonth(), 1));
|
||||||
|
}
|
||||||
|
const base = [20, 18, 22, 24, 23, 21, 15, 12, 9, 15, 24, 20];
|
||||||
|
const dummy = months.map((m, idx) => ({ key: m, id: `${m.getFullYear()}-${m.getMonth()}`, data: base[idx] }));
|
||||||
|
setSeries(dummy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = curMode === 'apps' ? 'app_executions' : 'workflow_executions';
|
||||||
|
const entries = await fetchSeriesForKey(key);
|
||||||
|
// Normalize variants: {Date, Value} or {date, value}
|
||||||
|
const normalized = (entries || []).map((d) => ({
|
||||||
|
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
|
||||||
|
value: Number(d?.Value ?? d?.value ?? 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If a month is selected, show DAILY bars for that month
|
||||||
|
if (selectedMonth instanceof Date) {
|
||||||
|
const year = selectedMonth.getFullYear();
|
||||||
|
const month = selectedMonth.getMonth();
|
||||||
|
|
||||||
|
// Build all days for selected month
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const nextMonthFirst = new Date(year, month + 1, 1);
|
||||||
|
const numDays = Math.round((nextMonthFirst - firstDay) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Sum values per day (normalize time to midnight)
|
||||||
|
const byDayKey = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
||||||
|
const sumPerDay = new Map();
|
||||||
|
normalized.forEach((p) => {
|
||||||
|
if (!p?.date || Number.isNaN(p.value)) return;
|
||||||
|
const d = p.date;
|
||||||
|
if (d.getFullYear() !== year || d.getMonth() !== month) return;
|
||||||
|
const dayKey = byDayKey(new Date(d.getFullYear(), d.getMonth(), d.getDate()));
|
||||||
|
sumPerDay.set(dayKey, (sumPerDay.get(dayKey) || 0) + p.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailySeries = Array.from({ length: numDays }, (_, i) => {
|
||||||
|
const d = new Date(year, month, i + 1);
|
||||||
|
const k = byDayKey(d);
|
||||||
|
const v = sumPerDay.get(k) || 0;
|
||||||
|
return { key: d, id: k, data: v };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure consistent ordering
|
||||||
|
setSeries(dailySeries.sort((a, b) => a.key - b.key));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, show MONTHLY aggregation for last 12 months including current month
|
||||||
|
const now = new Date();
|
||||||
|
const months = [];
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
months.push({ y: dt.getFullYear(), m: dt.getMonth(), key: new Date(dt.getFullYear(), dt.getMonth(), 1) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const byMonthKey = (d) => `${d.getFullYear()}-${d.getMonth()}`;
|
||||||
|
const sumPerMonth = new Map();
|
||||||
|
normalized.forEach((p) => {
|
||||||
|
if (!p?.date || Number.isNaN(p.value)) return;
|
||||||
|
const k = byMonthKey(new Date(p.date.getFullYear(), p.date.getMonth(), 1));
|
||||||
|
sumPerMonth.set(k, (sumPerMonth.get(k) || 0) + p.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthlySeries = months.map((mm) => {
|
||||||
|
const k = `${mm.y}-${mm.m}`;
|
||||||
|
const v = sumPerMonth.get(k) || 0;
|
||||||
|
return { key: mm.key, id: k, data: v };
|
||||||
|
});
|
||||||
|
|
||||||
|
setSeries(monthlySeries);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (monthOverride instanceof Date) {
|
||||||
|
// Clear series immediately to prevent visual overlap
|
||||||
|
setSeries([]);
|
||||||
|
setSelectedMonth(new Date(monthOverride.getFullYear(), monthOverride.getMonth(), 1));
|
||||||
|
setDays(370);
|
||||||
|
}
|
||||||
|
}, [monthOverride]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(mode);
|
||||||
|
}, [mode, globalUrl, days, selectedMonth, dummyMode]);
|
||||||
|
|
||||||
|
// Notify parent on loading changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof onLoadingChange === 'function') {
|
||||||
|
onLoadingChange(loading);
|
||||||
|
}
|
||||||
|
}, [loading, onLoadingChange]);
|
||||||
|
|
||||||
|
const barData = useMemo(() => (
|
||||||
|
(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' });
|
||||||
|
return { key: label, data: Number(d?.data || 0) };
|
||||||
|
})
|
||||||
|
), [series, mode, selectedMonth]);
|
||||||
|
|
||||||
|
// Build month dropdown options for last 12 months
|
||||||
|
const monthOptions = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const opts = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const dt = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
opts.push(dt);
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tooltip = <TooltipArea
|
||||||
|
tooltip={
|
||||||
|
<ChartTooltip
|
||||||
|
placement={"top"}
|
||||||
|
followCursor={true}
|
||||||
|
content={(data) => (
|
||||||
|
<div style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#1A1A1A",
|
||||||
|
border: "1px solid rgba(255,255,255,0.22)",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 8,
|
||||||
|
maxWidth: 240,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" style={{fontWeight: 600, color: "#ffffff"}}>{data?.x ?? ''}</Typography>
|
||||||
|
<Typography variant="body2" style={{color: "#ffffff"}}>{data?.y ?? ''}</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>Runs over time ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
exclusive
|
||||||
|
size="large"
|
||||||
|
value={mode}
|
||||||
|
onChange={(e, v) => v && setMode(v)}
|
||||||
|
sx={{
|
||||||
|
height: 37,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.22)',
|
||||||
|
borderRadius: '30px',
|
||||||
|
padding: '2px',
|
||||||
|
"& .MuiToggleButton-root": {
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "30px",
|
||||||
|
color: "#fff",
|
||||||
|
padding: "6px 16px",
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: "14px",
|
||||||
|
"&.Mui-selected": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
color: "#222",
|
||||||
|
fontWeight: "600",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="workflows">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
Workflows
|
||||||
|
</Box>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="apps">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
Apps
|
||||||
|
</Box>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} 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="runs-month-select-label">View Month</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="runs-month-select-label"
|
||||||
|
label="View Month"
|
||||||
|
value={selectedMonth ? selectedMonth.getTime() : 'ALL'}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
if (v === 'ALL') {
|
||||||
|
setSelectedMonth(null);
|
||||||
|
setDays(365);
|
||||||
|
} else {
|
||||||
|
const dt = new Date(Number(v));
|
||||||
|
setSelectedMonth(new Date(dt.getFullYear(), dt.getMonth(), 1));
|
||||||
|
setDays(370); // ensure enough historical range; backend param only hints
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem value={'ALL'}><em>All months</em></MenuItem>
|
||||||
|
{monthOptions.map((dt) => (
|
||||||
|
<MenuItem key={dt.getTime()} value={dt.getTime()}>
|
||||||
|
{dt.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<BarChart
|
||||||
|
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
|
||||||
|
height={300}
|
||||||
|
data={barData}
|
||||||
|
series={<BarSeries tooltip={tooltip} bar={<Bar rounded={true} />} />}
|
||||||
|
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
|
||||||
|
yAxis={
|
||||||
|
<LinearYAxis
|
||||||
|
tickSeries={
|
||||||
|
<LinearYAxisTickSeries
|
||||||
|
label={
|
||||||
|
<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
animated={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RunsOverTimeWidget;
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -14,15 +15,21 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField,
|
TextField,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FileCopy as FileCopyIcon,
|
FileCopy as FileCopyIcon,
|
||||||
OpenInNew as OpenInNewIcon,
|
OpenInNew as OpenInNewIcon,
|
||||||
Padding,
|
Refresh as RefreshIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
|
Check as CheckIcon,
|
||||||
} from "@mui/icons-material"
|
} from "@mui/icons-material"
|
||||||
|
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
|
||||||
import { Box, Skeleton, Typography } from '@mui/material';
|
import { Box, Skeleton, Typography } from '@mui/material';
|
||||||
import { Context } from '../context/ContextApi.jsx';
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
import RunDetectionTest from '../components/RunDetectionTest.jsx';
|
||||||
|
|
||||||
const SchedulesTab = memo((props) => {
|
const SchedulesTab = memo((props) => {
|
||||||
const {globalUrl, users, } = props;
|
const {globalUrl, users, } = props;
|
||||||
@@ -30,13 +37,58 @@ const SchedulesTab = memo((props) => {
|
|||||||
const [allSchedules, setAllSchedules] = React.useState([]);
|
const [allSchedules, setAllSchedules] = React.useState([]);
|
||||||
const [pipelines, setPipelines] = React.useState([]);
|
const [pipelines, setPipelines] = React.useState([]);
|
||||||
const [showLoader, setShowLoader] = React.useState(true);
|
const [showLoader, setShowLoader] = React.useState(true);
|
||||||
|
const [workflows, setWorkflows] = React.useState([]);
|
||||||
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
|
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 | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
|
||||||
|
|
||||||
|
const [ticketWebhook, setTicketWebhook] = React.useState("");
|
||||||
|
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
|
||||||
|
|
||||||
const { themeMode, brandColor } = useContext(Context);
|
const { themeMode, brandColor } = useContext(Context);
|
||||||
const theme = getTheme(themeMode, brandColor);
|
const theme = getTheme(themeMode, brandColor);
|
||||||
|
|
||||||
|
const handleGetWorkflows = () => {
|
||||||
|
const url = `${globalUrl}/api/v1/workflows`;
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for getting all workflows");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson.success !== false) {
|
||||||
|
setWorkflows(responseJson || []);
|
||||||
|
|
||||||
|
for (var i = 0; i < responseJson?.length; i++) {
|
||||||
|
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
|
||||||
|
|
||||||
|
for (var triggerkey in responseJson[i].triggers) {
|
||||||
|
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}`)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast(error.toString());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
handleGetWorkflows()
|
||||||
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
|
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
|
||||||
handleGetAllTriggers()
|
handleGetAllTriggers()
|
||||||
}
|
}
|
||||||
@@ -58,8 +110,11 @@ const SchedulesTab = memo((props) => {
|
|||||||
environment: pipeline.environment,
|
environment: pipeline.environment,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state === "start") toast("starting the pipeline");
|
if (state === "start") {
|
||||||
else toast.info("Stopping the pipeline. This may take a few minutes to propagate.")
|
toast("starting the pipeline")
|
||||||
|
} else {
|
||||||
|
toast.info("Stopping a pipeline. This may take a few minutes to propagate.")
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${globalUrl}/api/v1/triggers/pipeline`;
|
const url = `${globalUrl}/api/v1/triggers/pipeline`;
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
@@ -144,16 +199,65 @@ const SchedulesTab = memo((props) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle>
|
<DialogTitle style={{padding: "50px 50px 25px 50px", }}>
|
||||||
<Typography variant='h5' color="textPrimary" >
|
<Typography variant='h5' color="textPrimary" >
|
||||||
Run a Tenzir pipeline
|
Run a Tenzir pipeline
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
||||||
Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/pipelines" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
|
Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/explanations/architecture/pipeline/" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
|
||||||
</Typography>
|
</Typography>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent style={{padding: "0px 50px 50px 50px", }}>
|
||||||
<div>
|
<div style={{marginTop: 10, }}>
|
||||||
|
|
||||||
|
<Chip
|
||||||
|
onClick={() => {
|
||||||
|
setNewPipelineValue(`load_tcp "0.0.0.0:1514" { read_syslog } | import`)
|
||||||
|
}}
|
||||||
|
label={"Syslog Listener (TCP)"}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Chip
|
||||||
|
onClick={() => {
|
||||||
|
setNewPipelineValue(`load_udp "0.0.0.0:1514", insert_newlines=true | read_syslog | import`)
|
||||||
|
}}
|
||||||
|
label={"Syslog Listener (UDP)"}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Chip
|
||||||
|
onClick={() => {
|
||||||
|
setNewPipelineValue(`export live=true | sigma "/tmp/sigma_rules" | to "${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}"`)
|
||||||
|
}}
|
||||||
|
label={"Sigma Rules"}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Chip
|
||||||
|
onClick={() => {
|
||||||
|
setNewPipelineValue(`export live=true | to_opensearch "localhost:9200", action="create", index="shuffle_logs", user="admin", passwd="PASSWORD"`)
|
||||||
|
}}
|
||||||
|
label={"Opensearch Ingest"}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
color="primary"
|
color="primary"
|
||||||
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
|
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
|
||||||
@@ -163,8 +267,9 @@ const SchedulesTab = memo((props) => {
|
|||||||
minRows={4}
|
minRows={4}
|
||||||
required
|
required
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
defaultValue="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
|
defaultValue={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
|
||||||
placeholder="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
|
value={newPipelineValue}
|
||||||
|
placeholder={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
|
||||||
id="environment_name"
|
id="environment_name"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -174,7 +279,7 @@ const SchedulesTab = memo((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions style={{padding: "0px 50px 50px 50px", }}>
|
||||||
<Button
|
<Button
|
||||||
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
|
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -191,7 +296,7 @@ const SchedulesTab = memo((props) => {
|
|||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
Submit
|
Create Pipeline
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -232,18 +337,18 @@ const SchedulesTab = memo((props) => {
|
|||||||
})
|
})
|
||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
if (!responseJson.success && pipelineConfig.type !== "delete") {
|
if (!responseJson.success && pipelineConfig.type !== "delete") {
|
||||||
toast("Failed to set pipeline: " + responseJson.reason);
|
toast.error("Failed to set pipeline: " + responseJson.reason);
|
||||||
} else {
|
} else {
|
||||||
if (pipelineConfig.type === "create") {
|
if (pipelineConfig.type === "create") {
|
||||||
toast("Pipeline will be created: " + responseJson.reason)
|
toast.success("Pipeline will be created. Page will autorefresh in a bit: " + responseJson.reason)
|
||||||
setPipelineModalOpen(false)
|
setPipelineModalOpen(false)
|
||||||
|
|
||||||
} else if (pipelineConfig.type === "stop") {
|
} else if (pipelineConfig.type === "stop") {
|
||||||
toast("Pipeline will be stopped: " + responseJson.reason)
|
toast.success("Pipeline will be stopped: " + responseJson.reason)
|
||||||
setPipelineModalOpen(false)
|
setPipelineModalOpen(false)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
toast("Unknown pipeline type: " + pipelineConfig.type)
|
toast.info("Unknown pipeline type: " + pipelineConfig.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +379,7 @@ const SchedulesTab = memo((props) => {
|
|||||||
|
|
||||||
|
|
||||||
// Just use this one?
|
// Just use this one?
|
||||||
const url =
|
const url = `${globalUrl}/api/v1/workflows/${data?.workflow_id}/schedule/${data.id}`;
|
||||||
globalUrl +
|
|
||||||
"/api/v1/workflows/" +
|
|
||||||
data["workflow_id"] +
|
|
||||||
"/schedule/" +
|
|
||||||
data.id;
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -414,7 +514,7 @@ const SchedulesTab = memo((props) => {
|
|||||||
//toast(error.toString());
|
//toast(error.toString());
|
||||||
console.log("Get schedule error: ", error.toString());
|
console.log("Get schedule error: ", error.toString());
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const startWebHook = (trigger) => {
|
const startWebHook = (trigger) => {
|
||||||
const hookname = trigger.info.name;
|
const hookname = trigger.info.name;
|
||||||
@@ -490,8 +590,197 @@ const SchedulesTab = memo((props) => {
|
|||||||
Triggers are Automatic Workflow starters. <b>Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})</b>
|
Triggers are Automatic Workflow starters. <b>Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 50, marginBottom: 20 }}>
|
||||||
|
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
|
||||||
|
|
||||||
|
<Typography variant='body2' color="textSecondary" >
|
||||||
|
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="/docs/triggers#pipelines"
|
||||||
|
style={{ color: theme.palette.primary.main }}
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div style={{marginBottom: 10, marginTop: 10, }}/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={{}}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setPipelineModalOpen(true)}
|
||||||
|
>
|
||||||
|
Deploy New Pipeline
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{marginLeft: 10, }}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleGetAllTriggers()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshIcon style={{}}/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
marginTop: 24,
|
||||||
|
border: theme.palette.defaultBorder,
|
||||||
|
width: "100%",
|
||||||
|
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
|
||||||
|
paddingBottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
width: '100%',
|
||||||
|
tableLayout: "auto",
|
||||||
|
display: "table",
|
||||||
|
minWidth: pipelines?.length === 0 ? "auto" : 800,
|
||||||
|
overflowX: "auto",
|
||||||
|
paddingBottom: 0
|
||||||
|
}}>
|
||||||
|
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
|
||||||
|
{["Status", "Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
|
||||||
|
<ListItemText
|
||||||
|
key={index}
|
||||||
|
primary={header}
|
||||||
|
style={{
|
||||||
|
display: "table-cell",
|
||||||
|
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
borderBottom: theme.palette.defaultBorder,
|
||||||
|
position: "sticky",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ListItem>
|
||||||
|
{showLoader ? (
|
||||||
|
[...Array(6)].map((_, rowIndex) => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={rowIndex}
|
||||||
|
style={{
|
||||||
|
display: "table-row",
|
||||||
|
backgroundColor: theme.palette.platformColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array(5)
|
||||||
|
.fill()
|
||||||
|
.map((_, colIndex) => {
|
||||||
|
return (
|
||||||
|
<ListItemText
|
||||||
|
key={colIndex}
|
||||||
|
style={{
|
||||||
|
display: "table-cell",
|
||||||
|
padding: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
variant="text"
|
||||||
|
animation="wave"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.loaderColor,
|
||||||
|
height: "20px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemText>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
) : (
|
||||||
|
pipelines?.length === 0 ? (
|
||||||
|
<div style={{width: "100%", textAlign: "center", }}>
|
||||||
|
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipelines found.</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
):(
|
||||||
|
pipelines.map((pipeline, index) => {
|
||||||
|
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
|
||||||
|
<ListItemText
|
||||||
|
style={{ minWidth: 75, maxWidth: 75, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
|
||||||
|
primary={pipeline.state}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
style={{ maxWidth: 350, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
|
||||||
|
primary={pipeline.definition}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
style={{ display:'table-cell', padding: 8 }}
|
||||||
|
primary={pipeline.environment}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
style={{ display:'table-cell', }}
|
||||||
|
primary={pipeline.total_runs}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
style={{ display:'table-cell', }}
|
||||||
|
primary={(
|
||||||
|
<Box display="table-cell">
|
||||||
|
<Tooltip title={"Copy deletion command"} style={{}} aria-label={"Go to logs"}>
|
||||||
|
<IconButton style={{marginRight: 10, }} onClick={() => {
|
||||||
|
const copyContent = `curl -XPOST http://localhost:5160/api/v0/pipeline/delete -H "Content-Type: application/json" -d '{"id":"${pipeline.id}"}' -v`
|
||||||
|
const copyText = navigator?.clipboard?.writeText(copyContent)
|
||||||
|
if (copyText) {
|
||||||
|
toast.success("Pipeline copied to clipboard")
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to copy pipeline")
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<FileCopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={"Delete Pipeline"} style={{}} aria-label={"Go to logs"}>
|
||||||
|
<IconButton style={{marginRight: 10, }} onClick={() => {
|
||||||
|
changePipelineState(pipeline, "stop");
|
||||||
|
}}>
|
||||||
|
<DeleteIcon style={{color: red, }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<div style={{margin: 25, }}>
|
||||||
|
<RunDetectionTest
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
pipelines={pipelines}
|
||||||
|
workflows={workflows}
|
||||||
|
ticketWebhook={ticketWebhook}
|
||||||
|
detectionWorkflowId={detectionWorkflowId}
|
||||||
|
|
||||||
|
changePipelineState={changePipelineState}
|
||||||
|
submitPipelineWrapper={submitPipelineWrapper}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 0, fontWeight: 500}}>
|
<Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 50, fontWeight: 500}}>
|
||||||
Schedules
|
Schedules
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2' color="textSecondary">
|
<Typography variant='body2' color="textSecondary">
|
||||||
@@ -903,11 +1192,9 @@ const SchedulesTab = memo((props) => {
|
|||||||
style={{
|
style={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color:webhook.status === "running" ? '#1a1a1a' : null,
|
|
||||||
backgroundColor: webhook.status === "running" ? '#ff8544' : null,
|
|
||||||
width: 150,
|
width: 150,
|
||||||
}}
|
}}
|
||||||
color={webhook.status === "running" ? "secondary" : "primary"}
|
color={"secondary"}
|
||||||
variant={webhook.status === "running" ? "contained" : "outlined"}
|
variant={webhook.status === "running" ? "contained" : "outlined"}
|
||||||
disabled={webhook.status === "uninitialized"}
|
disabled={webhook.status === "uninitialized"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -929,166 +1216,7 @@ const SchedulesTab = memo((props) => {
|
|||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 50, marginBottom: 20 }}>
|
|
||||||
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
|
|
||||||
|
|
||||||
<Typography variant='body2' color="textSecondary" >
|
|
||||||
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="/docs/triggers#pipelines"
|
|
||||||
style={{ color: theme.palette.primary.main }}
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<div style={{marginBottom: 10, marginTop: 10, }}/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style={{ borderRadius: 4, textTransform: "capitalize", fontSize: 16, }}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => setPipelineModalOpen(true)}
|
|
||||||
>
|
|
||||||
Deploy New Pipeline
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderRadius: 4,
|
|
||||||
marginTop: 24,
|
|
||||||
border: theme.palette.defaultBorder,
|
|
||||||
width: "100%",
|
|
||||||
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
|
|
||||||
paddingBottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List
|
|
||||||
style={{
|
|
||||||
borderRadius: 4,
|
|
||||||
width: '100%',
|
|
||||||
tableLayout: "auto",
|
|
||||||
display: "table",
|
|
||||||
minWidth: pipelines?.length === 0 ? "auto" : 800,
|
|
||||||
overflowX: "auto",
|
|
||||||
paddingBottom: 0
|
|
||||||
}}>
|
|
||||||
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
|
|
||||||
{["Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
|
|
||||||
<ListItemText
|
|
||||||
key={index}
|
|
||||||
primary={header}
|
|
||||||
style={{
|
|
||||||
display: "table-cell",
|
|
||||||
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
borderBottom: theme.palette.defaultBorder,
|
|
||||||
position: "sticky",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ListItem>
|
|
||||||
{showLoader ? (
|
|
||||||
[...Array(6)].map((_, rowIndex) => {
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={rowIndex}
|
|
||||||
style={{
|
|
||||||
display: "table-row",
|
|
||||||
backgroundColor: theme.palette.platformColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array(5)
|
|
||||||
.fill()
|
|
||||||
.map((_, colIndex) => {
|
|
||||||
return (
|
|
||||||
<ListItemText
|
|
||||||
key={colIndex}
|
|
||||||
style={{
|
|
||||||
display: "table-cell",
|
|
||||||
padding: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Skeleton
|
|
||||||
variant="text"
|
|
||||||
animation="wave"
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.loaderColor,
|
|
||||||
height: "20px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItemText>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
): (
|
|
||||||
pipelines?.length === 0 ? (
|
|
||||||
<div style={{width: "100%", textAlign: "center", }}>
|
|
||||||
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipeline trigger found</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
):(
|
|
||||||
pipelines.map((pipeline, index) => {
|
|
||||||
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
|
|
||||||
if (index % 2 === 0) {
|
|
||||||
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
|
|
||||||
<ListItemText
|
|
||||||
style={{ display:'table-cell', padding: "8px 8px 8px 15px" }}
|
|
||||||
primary={pipeline.definition}
|
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
style={{ display:'table-cell', padding: 8 }}
|
|
||||||
primary={pipeline.environment}
|
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
style={{ display:'table-cell', }}
|
|
||||||
primary={pipeline.total_runs}
|
|
||||||
/>
|
|
||||||
<ListItemText
|
|
||||||
style={{ display:'table-cell', }}
|
|
||||||
primary={(
|
|
||||||
<Box display="table-cell">
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
textTransform: 'none',
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
variant={"outlined"}
|
|
||||||
disabled={pipeline.status === "uninitialized"}
|
|
||||||
onClick={() => {
|
|
||||||
changePipelineState(pipeline, "stop");
|
|
||||||
/*
|
|
||||||
if (pipeline.status === "running") {
|
|
||||||
changePipelineState(pipeline, "stop");
|
|
||||||
} else changePipelineState(pipeline, "start");
|
|
||||||
*/
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Stop Pipeline
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1096,3 +1224,4 @@ const SchedulesTab = memo((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default SchedulesTab;
|
export default SchedulesTab;
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ const CodeEditor = (props) => {
|
|||||||
|
|
||||||
// Auto-indent JSON-like content (with safety hehe)
|
// Auto-indent JSON-like content (with safety hehe)
|
||||||
const autoIndentContent = React.useCallback((content) => {
|
const autoIndentContent = React.useCallback((content) => {
|
||||||
|
return content
|
||||||
|
|
||||||
// Safety checks :)
|
// Safety checks :)
|
||||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||||
return content;
|
return content;
|
||||||
@@ -173,6 +175,7 @@ const CodeEditor = (props) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
|
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
|
||||||
|
|
||||||
// const {codelang, setcodelang} = props
|
// const {codelang, setcodelang} = props
|
||||||
@@ -1832,6 +1835,7 @@ const CodeEditor = (props) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
style={{
|
style={{
|
||||||
@@ -1842,6 +1846,35 @@ const CodeEditor = (props) => {
|
|||||||
File Editor ({localcodedata.length})
|
File Editor ({localcodedata.length})
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
right: 25,
|
||||||
|
top: 90,
|
||||||
|
zIndex: 5000,
|
||||||
|
}}
|
||||||
|
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
|
||||||
|
onClick={() => {
|
||||||
|
const indentedText = IndentJsonLikeString(localcodedata, 2)
|
||||||
|
if (indentedText !== undefined && indentedText !== null) {
|
||||||
|
setlocalcodedata(indentedText)
|
||||||
|
} else {
|
||||||
|
toast.warn("Could not indent the text. Please check the input format.", { autoClose: 5000 })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
title={"Indent Text"}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<FormatIndentIncreaseIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div
|
<div
|
||||||
@@ -2270,7 +2303,7 @@ const CodeEditor = (props) => {
|
|||||||
width: 50,
|
width: 50,
|
||||||
marginLeft: 100,
|
marginLeft: 100,
|
||||||
}}
|
}}
|
||||||
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
|
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const indentedText = IndentJsonLikeString(localcodedata, 2)
|
const indentedText = IndentJsonLikeString(localcodedata, 2)
|
||||||
if (indentedText !== undefined && indentedText !== null) {
|
if (indentedText !== undefined && indentedText !== null) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ import {
|
|||||||
Zoom,
|
Zoom,
|
||||||
Chip,
|
Chip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||||
|
|
||||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||||
@@ -172,6 +173,7 @@ const AppGrid = props => {
|
|||||||
// value={currentRefinement}
|
// value={currentRefinement}
|
||||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||||
var defaultSearch = ""
|
var defaultSearch = ""
|
||||||
|
const [inputValue, setInputValue] = useState("")
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||||
@@ -185,6 +187,12 @@ const AppGrid = props => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(currentRefinement || defaultSearch || "")
|
||||||
|
}, [currentRefinement])
|
||||||
|
|
||||||
|
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
|
||||||
|
|
||||||
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
|
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
|
||||||
//setLocalMessage(inputsearch)
|
//setLocalMessage(inputsearch)
|
||||||
refine(inputsearch)
|
refine(inputsearch)
|
||||||
@@ -217,12 +225,14 @@ const AppGrid = props => {
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
type="search"
|
type="search"
|
||||||
color="primary"
|
color="primary"
|
||||||
value={currentRefinement}
|
value={inputValue}
|
||||||
placeholder="Find Workflows..."
|
placeholder="Find Workflows..."
|
||||||
id="shuffle_search_field"
|
id="shuffle_search_field"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
removeQuery("q")
|
removeQuery("q")
|
||||||
refine(event.currentTarget.value)
|
const value = event.currentTarget.value
|
||||||
|
setInputValue(value)
|
||||||
|
debouncedRefine(value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if(event.key === "Enter") {
|
if(event.key === "Enter") {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useRef, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
export const useDebouncedCallback = (callback, delay = 300) => {
|
||||||
|
const timeoutRef = useRef(null);
|
||||||
|
const savedCallback = useRef(callback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return useCallback((...args) => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
savedCallback.current(...args);
|
||||||
|
}, delay);
|
||||||
|
}, [delay]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDebouncedCallback;
|
||||||
|
|
||||||
|
|
||||||
+557
-78
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext, memo } from "react";
|
import React, { useState, useEffect, useContext, memo } from "react";
|
||||||
import { Context } from "../context/ContextApi.jsx";
|
import { Context } from "../context/ContextApi.jsx";
|
||||||
|
import AuthenticationModal from "../components/AuthenticationModal.jsx";
|
||||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||||
import { getTheme } from "../theme.jsx";
|
import { getTheme } from "../theme.jsx";
|
||||||
import { toast } from "react-toastify"
|
import { toast } from "react-toastify"
|
||||||
@@ -21,12 +22,16 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CheckCircle as CheckCircleIcon,
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Check as CheckIcon,
|
||||||
HourglassDisabled as HourglassDisabledIcon,
|
HourglassDisabled as HourglassDisabledIcon,
|
||||||
RestartAlt as RestartAltIcon,
|
RestartAlt as RestartAltIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
ExpandMore as ExpandMoreIcon,
|
||||||
ExpandLess as ExpandLessIcon,
|
ExpandLess as ExpandLessIcon,
|
||||||
Send as SendIcon,
|
Send as SendIcon,
|
||||||
Error as ErrorIcon,
|
Error as ErrorIcon,
|
||||||
|
Close as CloseIcon,
|
||||||
|
OpenInNew as OpenInNewIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,21 +48,26 @@ const AgentUI = (props) => {
|
|||||||
const [data, setData] = useState({})
|
const [data, setData] = useState({})
|
||||||
const [openIndexes, setOpenIndexes] = useState([])
|
const [openIndexes, setOpenIndexes] = useState([])
|
||||||
const [disableButtons, setDisableButtons] = useState(false)
|
const [disableButtons, setDisableButtons] = useState(false)
|
||||||
|
const [apps, setApps] = useState([])
|
||||||
|
const [appAuth, setAppAuth] = useState([])
|
||||||
|
|
||||||
const [originalStartTime, setOriginalStartTime] = useState(0)
|
|
||||||
const [latestEndTime, setLatestEndTime] = useState(0)
|
|
||||||
const [showAgentStarter, setShowAgentStarter] = useState(false)
|
const [showAgentStarter, setShowAgentStarter] = useState(false)
|
||||||
const [actionInput, setActionInput] = useState("")
|
const [actionInput, setActionInput] = useState("")
|
||||||
|
const [questionAnswers, setQuestionAnswers] = useState({})
|
||||||
|
|
||||||
const {themeMode} = useContext(Context)
|
const {themeMode} = useContext(Context)
|
||||||
const theme = getTheme(themeMode)
|
const theme = getTheme(themeMode)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
document.title = "Shuffle AI Agents"
|
||||||
|
|
||||||
const agentWrapperStyle = {
|
const agentWrapperStyle = {
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 1000,
|
height: 1000,
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
paddingTop: 100,
|
paddingTop: 100,
|
||||||
|
paddingBottom: 1000,
|
||||||
|
backgroundColor: theme.palette.backgroundColor,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.input === undefined || data.input === null) {
|
if (data.input === undefined || data.input === null) {
|
||||||
@@ -75,7 +85,22 @@ const AgentUI = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node_id === undefined || node_id === null || node_id === "") {
|
if (node_id === undefined || node_id === null || node_id === "") {
|
||||||
return
|
// Look for AI agent
|
||||||
|
/*
|
||||||
|
for (var key in execution_data.results) {
|
||||||
|
const item = execution_data.results[key]
|
||||||
|
if (item?.action?.app_name !== "AI Agent") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
node_id = item?.action?.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (node_id === undefined || node_id === null || node_id === "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var found = false
|
var found = false
|
||||||
@@ -150,15 +175,22 @@ const AgentUI = (props) => {
|
|||||||
if (responseJson.success !== false) {
|
if (responseJson.success !== false) {
|
||||||
if (responseJson.status === "EXECUTING") {
|
if (responseJson.status === "EXECUTING") {
|
||||||
// Recursively looking for updates until it's not executing anymore
|
// Recursively looking for updates until it's not executing anymore
|
||||||
setTimeout(() => {
|
//setTimeout(() => {
|
||||||
GetExecution(execution_id, node_id, authorization)
|
// GetExecution(execution_id, node_id, authorization)
|
||||||
}, 3000)
|
//}, 3000)
|
||||||
} else {
|
} else {
|
||||||
setDisableButtons(false)
|
setDisableButtons(false)
|
||||||
setDisableButtons(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setExecution(responseJson)
|
try {
|
||||||
|
if (JSON.stringify(responseJson) !== JSON.stringify(execution)) {
|
||||||
|
setExecution(responseJson)
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Error comparing executions: ", e)
|
||||||
|
setExecution(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
findNodeData(responseJson, node_id)
|
findNodeData(responseJson, node_id)
|
||||||
} else {
|
} else {
|
||||||
setDisableButtons(false)
|
setDisableButtons(false)
|
||||||
@@ -216,12 +248,53 @@ const AgentUI = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||||
|
setTimeout(() => {
|
||||||
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||||
|
}, 10000)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error("Error: " + error)
|
toast.error("Error: " + error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAppAuth = () => {
|
||||||
|
const url = `${globalUrl}/api/v1/apps/authentication`
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson.success !== false) {
|
||||||
|
setAppAuth(responseJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error("Error in auth load: " + error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApps = () => {
|
||||||
|
const url = `${globalUrl}/api/v1/apps`
|
||||||
|
fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson.success !== false) {
|
||||||
|
setApps(responseJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error("Error in app load: " + error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const executionId = params.get("execution_id")
|
const executionId = params.get("execution_id")
|
||||||
@@ -233,9 +306,15 @@ const AgentUI = (props) => {
|
|||||||
setShowAgentStarter(true)
|
setShowAgentStarter(true)
|
||||||
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
|
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getApps()
|
||||||
|
getAppAuth()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const maxTimelineWidth = 150
|
const maxTimelineWidth = 300
|
||||||
|
|
||||||
|
var latestEndTime = 0
|
||||||
|
var originalStartTime = 0
|
||||||
const TimelineItem = (props) => {
|
const TimelineItem = (props) => {
|
||||||
const { item, index } = props;
|
const { item, index } = props;
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
@@ -258,12 +337,96 @@ const AgentUI = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
const categoryStyle = {
|
const categoryStyle = {
|
||||||
width: 20,
|
width: 25,
|
||||||
height: 20,
|
height: 25,
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
|
borderRadius: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedCategory = item.category === "singul" ?
|
|
||||||
|
const validate = validateJson(item.details)
|
||||||
|
const itemStartTime = item.start_time
|
||||||
|
var itemEndTime = item.end_time
|
||||||
|
if (item.category === "agent" && itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
||||||
|
console.log("Rerender 1: ", itemStartTime, originalStartTime)
|
||||||
|
originalStartTime = itemStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
||||||
|
console.log("Rerender 2")
|
||||||
|
latestEndTime = itemEndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemEndTime === undefined || itemEndTime === null) {
|
||||||
|
// Set it to now
|
||||||
|
itemEndTime = latestEndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.category == "agent" && itemEndTime === 0) {
|
||||||
|
// Right now -> .toLocaleString() support
|
||||||
|
itemEndTime = Date.now() / 1000
|
||||||
|
|
||||||
|
//<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
||||||
|
|
||||||
|
if (itemEndTime > latestEndTime) {
|
||||||
|
latestEndTime = itemEndTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDuration = latestEndTime - originalStartTime
|
||||||
|
var currentDuration = itemStartTime - itemEndTime
|
||||||
|
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
||||||
|
//var timelineMarginLeft = 0
|
||||||
|
|
||||||
|
// Calculate how long the div should be
|
||||||
|
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
||||||
|
|
||||||
|
//console.log("CURRENT DURATION (1): ", currentDuration, itemStartTime, itemEndTime, originalStartTime, latestEndTime, totalDuration, timelineMarginLeft, timelineWidth)
|
||||||
|
if (totalDuration === currentDuration) {
|
||||||
|
timelineMarginLeft = 0
|
||||||
|
timelineWidth = maxTimelineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just for simplicity's sake
|
||||||
|
if (currentDuration < -1000000 || currentDuration > 1000000) {
|
||||||
|
currentDuration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDuration < 0) {
|
||||||
|
currentDuration = currentDuration * -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTopPadding = 10
|
||||||
|
const open = openIndexes.includes(index)
|
||||||
|
|
||||||
|
var questions = []
|
||||||
|
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||||
|
item.type = "finalise"
|
||||||
|
item.category = "finalise"
|
||||||
|
item.label = item?.details?.reason || item.label
|
||||||
|
|
||||||
|
} else if (item?.category === "ask" || item?.details?.action === "ask") {
|
||||||
|
|
||||||
|
item.type = "question"
|
||||||
|
item.category = "ask"
|
||||||
|
item.label = item?.details?.reason || item.label
|
||||||
|
|
||||||
|
for (var fieldKey in item?.details?.fields) {
|
||||||
|
const field = item?.details?.fields[fieldKey]
|
||||||
|
if (field?.key !== "question") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
questions.push({
|
||||||
|
"question": field?.value,
|
||||||
|
"index": questions.length + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item?.details?.action === "api" && item?.details?.tool?.length > 0) {
|
||||||
|
item.label = item?.details?.reason || item.label
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedCategory = item.category === "singul" ?
|
||||||
<Tooltip title="Singul" placement="top">
|
<Tooltip title="Singul" placement="top">
|
||||||
<img src="/images/logos/singul.svg" style={categoryStyle} />
|
<img src="/images/logos/singul.svg" style={categoryStyle} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -273,38 +436,167 @@ const AgentUI = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
:
|
:
|
||||||
<div style={categoryStyle} />
|
<div style={categoryStyle} />
|
||||||
|
|
||||||
const validate = validateJson(item.details)
|
var showAuthentication = false
|
||||||
const itemStartTime = item.start_time
|
var selectedApp = {}
|
||||||
var itemEndTime = item.end_time
|
if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
|
||||||
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
|
||||||
console.log("Rerender 1")
|
// Find the app and inject the image
|
||||||
//setOriginalStartTime(itemStartTime)
|
const toolName = item.details.tool.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||||
|
for (var appKey in apps) {
|
||||||
|
const app = apps[appKey]
|
||||||
|
|
||||||
|
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||||
|
if (appname !== toolName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.large_image === undefined || app.large_image === null || app.large_image.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedApp = app
|
||||||
|
|
||||||
|
// Override the category
|
||||||
|
//item.category = app.name
|
||||||
|
//item.label = item?.details?.reason || item.label
|
||||||
|
parsedCategory =
|
||||||
|
<Tooltip title={app.name} placement="top">
|
||||||
|
<img src={app.large_image} style={categoryStyle} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
if (!showAuthentication) {
|
||||||
console.log("Rerender 2")
|
if (item?.details?.run_details?.raw_response !== undefined && item?.details?.run_details?.raw_response !== null && item?.details?.run_details?.raw_response?.includes("app_authentication")) {
|
||||||
setLatestEndTime(itemEndTime)
|
showAuthentication = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemEndTime === undefined || itemEndTime === null) {
|
var questionSubmitDisabled = questions.length === 0 ? true : false
|
||||||
// Set it to now
|
for (var qKey in questions) {
|
||||||
itemEndTime = latestEndTime
|
const q = questions[qKey]
|
||||||
|
if (questionAnswers[q.question] === undefined || questionAnswers[q.question] === null || questionAnswers[q.question] === "") {
|
||||||
|
//console.log("EMPTY QUESTION: ", q)
|
||||||
|
questionSubmitDisabled = true
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
questionSubmitDisabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDuration = latestEndTime - originalStartTime
|
const barColor = item.status === "FINISHED" ? green :
|
||||||
const currentDuration = itemStartTime - itemEndTime
|
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
||||||
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
||||||
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
theme.palette.surfaceColor
|
||||||
|
|
||||||
if (totalDuration === currentDuration) {
|
const rerunAgentButton =
|
||||||
timelineMarginLeft = 0
|
<Tooltip title="Rerun from the start with the same input" placement="right">
|
||||||
timelineWidth = maxTimelineWidth
|
<span>
|
||||||
|
<IconButton
|
||||||
|
style={{marginLeft: 20, }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
toast.info("Attempting to rerun everything.")
|
||||||
|
setDisableButtons(true)
|
||||||
|
|
||||||
|
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
|
||||||
|
toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.")
|
||||||
|
} else {
|
||||||
|
//console.log("DETAILS: ", item?.details)
|
||||||
|
for (var messagekey in item?.details?.input?.messages) {
|
||||||
|
const message = item?.details?.input?.messages[messagekey]
|
||||||
|
if (message.role === "user") {
|
||||||
|
setActionInput(message.content)
|
||||||
|
setDisableButtons(true)
|
||||||
|
|
||||||
|
submitInput(message.content)
|
||||||
|
//toast.info("Rerun started. Please wait a few seconds and this page should refresh automatically.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RestartAltIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
|
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">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={item.type !== "decision" || disableButtons}
|
||||||
|
style={{marginLeft: 20, }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
//toast.info("Attempting to rerun this decision by itself.")
|
||||||
|
setDisableButtons(true)
|
||||||
|
RerunDecision(item.details)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTopPadding = 10
|
|
||||||
const open = openIndexes.includes(index)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -315,7 +607,7 @@ const AgentUI = (props) => {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (!hovered) {
|
if (!hovered) {
|
||||||
console.log("HOVER")
|
//console.log("HOVER")
|
||||||
setHovered(true)
|
setHovered(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -364,41 +656,50 @@ const AgentUI = (props) => {
|
|||||||
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
||||||
{parsedCategory}
|
{parsedCategory}
|
||||||
</div>
|
</div>
|
||||||
|
{/*
|
||||||
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
|
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
|
||||||
{/* To ISO string from unix time */}
|
{item?.start_time !== undefined && item?.start_time !== null && item?.start_time !== 0 ?
|
||||||
{new Date(item.start_time * 1000).toLocaleString()}
|
new Date(item.start_time * 1000).toLocaleString()
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
||||||
<Chip
|
<Chip
|
||||||
label={item.type}
|
label={item.type}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
minWidth: 200,
|
minWidth: 300,
|
||||||
maxWidth: 200,
|
maxWidth: 300,
|
||||||
paddingTop: defaultTopPadding,
|
paddingTop: defaultTopPadding,
|
||||||
}}>
|
}}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip title={`Time taken: ${currentDuration*-1} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right">
|
||||||
<div style={{
|
<div style={{
|
||||||
minWidth: maxTimelineWidth,
|
minWidth: maxTimelineWidth,
|
||||||
maxWidth: maxTimelineWidth,
|
maxWidth: maxTimelineWidth,
|
||||||
paddingTop: defaultTopPadding*1.3,
|
paddingTop: defaultTopPadding*1.5,
|
||||||
}}>
|
}}>
|
||||||
{currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
{currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: item.status === "FINISHED" ?
|
backgroundColor: barColor,
|
||||||
green : item.status === "RUNNING" || item.status === "" ?
|
|
||||||
theme.palette.main : theme.palette.surfaceColor,
|
|
||||||
|
|
||||||
marginLeft: timelineMarginLeft,
|
marginLeft: timelineMarginLeft,
|
||||||
minWidth: timelineWidth,
|
minWidth: timelineWidth,
|
||||||
maxWidth: timelineWidth,
|
maxWidth: timelineWidth,
|
||||||
height: 10,
|
minHeight: 10,
|
||||||
}} />
|
maxHeight: 10,
|
||||||
: null}
|
borderRadius: theme.palette.borderRadius,
|
||||||
|
}}>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@@ -407,24 +708,68 @@ const AgentUI = (props) => {
|
|||||||
maxWidth: 100,
|
maxWidth: 100,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
}}>
|
}}>
|
||||||
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="left">
|
{item.category === "ask" ?
|
||||||
<span>
|
<span style={{display: "flex", }}>
|
||||||
<IconButton
|
{rerunButton}
|
||||||
disabled={item.type !== "decision" || disableButtons}
|
{/*
|
||||||
style={{marginLeft: 20, }}
|
<Tooltip title="Approve" placement="left">
|
||||||
onClick={(e) => {
|
<span>
|
||||||
e.preventDefault()
|
<IconButton
|
||||||
e.stopPropagation()
|
disabled={disableButtons}
|
||||||
|
style={{marginLeft: 20, }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
toast.info("Attempting to rerun this decision by itself.")
|
toast.info("Approving this step.")
|
||||||
setDisableButtons(true)
|
}}
|
||||||
RerunDecision(item.details)
|
>
|
||||||
}}
|
<CheckIcon style={{color: green, }} />
|
||||||
>
|
</IconButton>
|
||||||
<RestartAltIcon />
|
</span>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
|
<Tooltip title="Deny" placement="left">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={item.type !== "decision" || disableButtons}
|
||||||
|
style={{marginLeft: 0, }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
toast.info("Stopping on this step.")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon style={{color: red, }} />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Tooltip title="See in another window" placement="left">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
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
|
||||||
|
const newurl = `/forms/${execution?.workflow?.id}?authorization=${execution.authorization}&reference_execution=${execution.execution_id}&source_node=${agentActionResult?.action?.id}&decision_id=${item.details.run_details.id}&backend_url=${globalUrl}`
|
||||||
|
window.open(newurl, '_blank', 'noopener,noreferrer');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OpenInNewIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
:
|
||||||
|
item.category === "agent" ?
|
||||||
|
rerunAgentButton
|
||||||
|
:
|
||||||
|
rerunButton
|
||||||
|
}
|
||||||
<Tooltip title="Explore results" placement="right">
|
<Tooltip title="Explore results" placement="right">
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -442,6 +787,69 @@ const AgentUI = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showAuthentication && selectedApp.id !== undefined ?
|
||||||
|
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 25, }}>
|
||||||
|
<AuthenticationModal
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
userdata={userdata}
|
||||||
|
|
||||||
|
setAppAuthentication={setAppAuth}
|
||||||
|
selectedAppData={selectedApp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{questions?.length > 0 && item?.status === "RUNNING" ?
|
||||||
|
<div>
|
||||||
|
{questions.map((q, questionIndex) => {
|
||||||
|
return (
|
||||||
|
<div style={{marginTop: 25, }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{`${q.question}`}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={`Question ${q.index}`}
|
||||||
|
placeholder="No question found"
|
||||||
|
variant="outlined"
|
||||||
|
style={{width: 800, marginTop: 20, }}
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
defaultValue={questionAnswers[q.question]?.value || ""}
|
||||||
|
onBlur={(e) => {
|
||||||
|
console.log("Change: ", e.target.value)
|
||||||
|
try {
|
||||||
|
questionAnswers[q.question] = {
|
||||||
|
"index": questionIndex,
|
||||||
|
"value": e.target.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuestionAnswers({...questionAnswers, })
|
||||||
|
} catch (e) {
|
||||||
|
toast.warn("Something went wrong. Please contact support@shuffler.io. Details: " + e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
style={{marginTop: 10, }}
|
||||||
|
disabled={questionSubmitDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
|
||||||
|
|
||||||
{open ?
|
{open ?
|
||||||
<div style={{marginTop: 10, marginBottom: 10, }}>
|
<div style={{marginTop: 10, marginBottom: 10, }}>
|
||||||
@@ -479,7 +887,12 @@ const AgentUI = (props) => {
|
|||||||
const TimelineRender = (props) => {
|
const TimelineRender = (props) => {
|
||||||
const { agent_data } = props;
|
const { agent_data } = props;
|
||||||
|
|
||||||
const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||||
|
const validate = validateJson(actionResult?.result)
|
||||||
|
if (validate.valid === true) {
|
||||||
|
actionResult.result = validate.result
|
||||||
|
}
|
||||||
|
|
||||||
var timelineItems = [
|
var timelineItems = [
|
||||||
{
|
{
|
||||||
"label": "AI Agent 2",
|
"label": "AI Agent 2",
|
||||||
@@ -493,6 +906,27 @@ const AgentUI = (props) => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Setting up the initial item
|
||||||
|
if (agent_data?.started_at === undefined && execution?.started_at !== undefined) {
|
||||||
|
timelineItems[0].start_time = execution?.started_at
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent_data?.completed_at === undefined && execution?.completed_at !== undefined) {
|
||||||
|
timelineItems[0].end_time = execution?.completed_at
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always prioritise the execution status first
|
||||||
|
// agent (RUNNING) = workflow (EXECUTING)
|
||||||
|
if (execution?.status !== undefined) {
|
||||||
|
timelineItems[0].status = execution?.status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionResult?.result?.status !== undefined && actionResult?.result?.status !== null && actionResult?.result?.status?.length > 0) {
|
||||||
|
if (timelineItems[0].status !== "FINISHED" && timelineItems[0].status !== "ABORTED" && timelineItems[0].status !== "FAILURE") {
|
||||||
|
timelineItems[0].status = actionResult?.result?.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Autofixer for result lol
|
// Autofixer for result lol
|
||||||
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
|
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
|
||||||
const verifiedInput = validateJson(actionResult?.result)
|
const verifiedInput = validateJson(actionResult?.result)
|
||||||
@@ -500,6 +934,7 @@ const AgentUI = (props) => {
|
|||||||
agent_data.decisions = verifiedInput.result?.decisions
|
agent_data.decisions = verifiedInput.result?.decisions
|
||||||
|
|
||||||
setAgentActionResult(actionResult)
|
setAgentActionResult(actionResult)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,13 +951,13 @@ const AgentUI = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var newTimelineItem = {
|
var newTimelineItem = {
|
||||||
"label": item.action,
|
"label": item?.action,
|
||||||
"type": "decision",
|
"type": "decision",
|
||||||
"category": item.category,
|
"category": item?.category,
|
||||||
|
|
||||||
"status": item.run_details.status,
|
"status": item?.run_details?.status,
|
||||||
"start_time": item.run_details.started_at,
|
"start_time": item?.run_details?.started_at,
|
||||||
"end_time": item.run_details.completed_at,
|
"end_time": item?.run_details?.completed_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
newTimelineItem.details = item
|
newTimelineItem.details = item
|
||||||
@@ -577,6 +1012,14 @@ const AgentUI = (props) => {
|
|||||||
setAgentRequestLoading(true)
|
setAgentRequestLoading(true)
|
||||||
//setShowAgentStarter(false);
|
//setShowAgentStarter(false);
|
||||||
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
|
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
|
||||||
|
//
|
||||||
|
setData({})
|
||||||
|
setExecution(null)
|
||||||
|
setAgentRequestLoading(true)
|
||||||
|
setShowAgentStarter(true)
|
||||||
|
setActionInput(inputText)
|
||||||
|
|
||||||
|
setAgentActionResult(null)
|
||||||
|
|
||||||
if (inputText === undefined || inputText === null || inputText === "") {
|
if (inputText === undefined || inputText === null || inputText === "") {
|
||||||
toast.error("Please provide a valid input for the AI Agent.")
|
toast.error("Please provide a valid input for the AI Agent.")
|
||||||
@@ -606,7 +1049,7 @@ const AgentUI = (props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"action",
|
"name":"action",
|
||||||
"value":"list_tickets"
|
"value":"list_tickets,API"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|
||||||
@@ -637,18 +1080,38 @@ const AgentUI = (props) => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
||||||
|
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
||||||
|
if (isCmdEnter || isCtrlEnter) {
|
||||||
|
e.preventDefault()
|
||||||
|
submitInput(actionInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={agentWrapperStyle}>
|
<div style={agentWrapperStyle}>
|
||||||
|
<TextField
|
||||||
|
id="copy_element_shuffle"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
|
||||||
{showAgentStarter ?
|
{showAgentStarter ?
|
||||||
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => {
|
<Box
|
||||||
e.preventDefault();
|
component="form"
|
||||||
submitInput(actionInput);
|
style={{textAlign: "center", }}
|
||||||
}}>
|
onKeyDown={handleKeyDown}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitInput(actionInput);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img src="/images/logos/agent.svg" style={{
|
<img src="/images/logos/agent.svg" style={{
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
|
borderRadius: theme.palette.borderRadius,
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
<Typography variant="h5" style={{marginTop: 30, }}>
|
<Typography variant="h5" style={{marginTop: 30, }}>
|
||||||
@@ -661,7 +1124,7 @@ const AgentUI = (props) => {
|
|||||||
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
minRows={2}
|
||||||
defaultValue={execution?.execution_id || ""}
|
defaultValue={actionInput || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setActionInput(e.target.value)
|
setActionInput(e.target.value)
|
||||||
}}
|
}}
|
||||||
@@ -704,6 +1167,22 @@ const AgentUI = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<Tooltip title="Reload the agent data" placement="top">
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
disabled={execution === null || Object.keys(execution).length === 0}
|
||||||
|
style={{marginLeft: 25, }}
|
||||||
|
variant={"outlined"}
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{buttonState === "timeline" ?
|
{buttonState === "timeline" ?
|
||||||
<TimelineRender agent_data={data} />
|
<TimelineRender agent_data={data} />
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
|||||||
|
|
||||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||||
import algoliasearch from 'algoliasearch/lite';
|
import algoliasearch from 'algoliasearch/lite';
|
||||||
|
import useDebouncedCallback from "../utils/useDebouncedCallback.jsx";
|
||||||
import {
|
import {
|
||||||
Zoom,
|
Zoom,
|
||||||
Fade,
|
Fade,
|
||||||
@@ -275,7 +276,7 @@ export const triggers = [
|
|||||||
{
|
{
|
||||||
"name": "alertinfo",
|
"name": "alertinfo",
|
||||||
"example": "",
|
"example": "",
|
||||||
"value": "Do you want to continue the workflow? Start parameters: $exec",
|
"value": "## Stop or continue?\n\nDetails: $exec",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "options",
|
"name": "options",
|
||||||
@@ -1259,6 +1260,24 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
"multiline": true,
|
"multiline": true,
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
// An attempt at handling APIs directly. This ~kind of works
|
||||||
|
{
|
||||||
|
"name": "API",
|
||||||
|
"description": "Attempts to take your fields and run an API call with them, whatever they are",
|
||||||
|
"label": "Custom Action",
|
||||||
|
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "fields",
|
||||||
|
"value": "",
|
||||||
|
"description": "A JSON object with the fields to send to the API. Example: {\"url\": \"hello\", \"key2\": \"value2\"}",
|
||||||
|
"required": true,
|
||||||
|
"multiline": true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
*/
|
||||||
{
|
{
|
||||||
"name": "Translate standard",
|
"name": "Translate standard",
|
||||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||||
@@ -11589,10 +11608,16 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStop = (e, app) => {
|
const handleDragStop = (e, app) => {
|
||||||
|
if (cy === undefined || cy == null) {
|
||||||
|
console.log("Cytoscape not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var currentnode = cy.getElementById(newNodeId);
|
var currentnode = cy.getElementById(newNodeId);
|
||||||
|
|
||||||
if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
|
if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
|
||||||
return;
|
console.log("No current node found")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) {
|
if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) {
|
||||||
@@ -12411,11 +12436,20 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
|
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
|
||||||
|
const debouncedRefine = useDebouncedCallback(refine, 500)
|
||||||
|
const lastRefinedRef = useRef(currentRefinement)
|
||||||
|
|
||||||
|
const safeRefine = (value) => {
|
||||||
|
if (value === lastRefinedRef.current) return
|
||||||
|
lastRefinedRef.current = value
|
||||||
|
debouncedRefine(value)
|
||||||
|
}
|
||||||
|
|
||||||
if (document !== undefined) {
|
if (document !== undefined) {
|
||||||
const appsearchValue = document.getElementById("appsearch")
|
const appsearchValue = document.getElementById("appsearch")
|
||||||
if (appsearchValue !== undefined && appsearchValue !== null) {
|
if (appsearchValue !== undefined && appsearchValue !== null) {
|
||||||
if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) {
|
if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) {
|
||||||
refine(appsearchValue.value)
|
safeRefine(appsearchValue.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12448,8 +12482,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
//if (event.currentTarget.value.length > 0 && !searchOpen) {
|
//if (event.currentTarget.value.length > 0 && !searchOpen) {
|
||||||
// setSearchOpen(true)
|
// setSearchOpen(true)
|
||||||
//}
|
//}
|
||||||
|
safeRefine(event.currentTarget.value)
|
||||||
refine(event.currentTarget.value)
|
|
||||||
}}
|
}}
|
||||||
limit={5}
|
limit={5}
|
||||||
/>
|
/>
|
||||||
@@ -14725,7 +14758,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Conditions can't be used for loops [ .# ]{" "}
|
<b>PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "}</b>
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -20236,17 +20269,23 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
|
const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
border: theme.palette.DialogStyle.border,
|
border: theme.palette.DialogStyle.border,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 100,
|
bottom: 100,
|
||||||
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
||||||
|
width: "fit-content",
|
||||||
|
maxWidth: "45vw",
|
||||||
|
minWidth: 300,
|
||||||
|
|
||||||
color: theme.palette.DialogStyle.color,
|
color: theme.palette.DialogStyle.color,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
transition: "left 0.3s ease, top 0.3s ease",
|
transition: "left 0.3s ease, top 0.3s ease",
|
||||||
}}
|
overflowWrap: "anywhere",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -22726,12 +22765,12 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
style={{ float: "right", marginTop: 20, }}
|
style={{ float: "right", marginTop: 20, }}
|
||||||
|
|
||||||
// Max 5 days in the past
|
// Max 5 days in the past
|
||||||
disabled={userdata.region_url !== "https://shuffler.io" || executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)}
|
disabled={executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toast("Opening logs in a new tab")
|
toast("Opening logs in a new tab")
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
window.open(`${globalUrl}/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
||||||
}, 250)
|
}, 250)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1494,13 +1494,13 @@ const ApiExplorerWrapper = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: theme.palette.inputColor,
|
backgroundColor: theme.palette.inputColor,
|
||||||
padding: 15,
|
padding: 15,
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
marginBottom: 30,
|
marginBottom: 30,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" style={{marginBottom: 25, }}>
|
<Typography variant="h6" style={{marginBottom: 25, }}>
|
||||||
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
|
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
+103
-107
@@ -8,6 +8,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Divider,
|
Divider,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -2680,11 +2681,11 @@ const AppCreator = (defaultprops) => {
|
|||||||
setErrorCode(responseJson.reason);
|
setErrorCode(responseJson.reason);
|
||||||
|
|
||||||
if (responseJson?.details !== undefined && responseJson?.details !== null) {
|
if (responseJson?.details !== undefined && responseJson?.details !== null) {
|
||||||
toast.error("Failed to build - contact support@shuffler.io: " + responseJson.details, {
|
toast.error("Failed to build - contact support@shuffler.io:\n\n" + responseJson.details, {
|
||||||
autoClose: 60000
|
autoClose: 60000
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
toast.error("Failed to build: " + responseJson.reason, {
|
toast.error("Failed to build: \n\n" + responseJson?.reason, {
|
||||||
autoClose: 10000
|
autoClose: 10000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2930,7 +2931,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
Query
|
Query
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
<div style={{ display: "flex", width: 100 }}>
|
<ButtonGroup style={{ display: "flex", width: 100 }}>
|
||||||
{index === extraAuth.length - 1 ? (
|
{index === extraAuth.length - 1 ? (
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -2963,7 +2964,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
>
|
>
|
||||||
<RemoveIcon style={{}} />
|
<RemoveIcon style={{}} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -3431,13 +3432,13 @@ const AppCreator = (defaultprops) => {
|
|||||||
const ActionPaper = (props) => {
|
const ActionPaper = (props) => {
|
||||||
const { data, index } = props
|
const { data, index } = props
|
||||||
|
|
||||||
const [updater, setUpdater] = useState("tmp");
|
const [updater, setUpdater] = useState("tmp");
|
||||||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||||||
const [urlPath, setUrlPath] = useState("");
|
const [urlPath, setUrlPath] = useState("");
|
||||||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||||||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||||||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||||
const [currentAction, setCurrentAction] = useState({
|
const [currentAction, setCurrentAction] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
file_field: "",
|
file_field: "",
|
||||||
@@ -3454,6 +3455,10 @@ const AppCreator = (defaultprops) => {
|
|||||||
required_bodyfields: [],
|
required_bodyfields: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Queries: ", urlPathQueries)
|
||||||
|
}, [urlPathQueries])
|
||||||
|
|
||||||
const findBodyParams = (body) => {
|
const findBodyParams = (body) => {
|
||||||
const regex = /\${(\w+)}/g;
|
const regex = /\${(\w+)}/g;
|
||||||
const found = body.match(regex);
|
const found = body.match(regex);
|
||||||
@@ -3462,7 +3467,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
} else {
|
} else {
|
||||||
setExtraBodyFields(found);
|
setExtraBodyFields(found);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const UrlPathParameters = () => {
|
const UrlPathParameters = () => {
|
||||||
const values = getCurrentPaths(urlPath);
|
const values = getCurrentPaths(urlPath);
|
||||||
@@ -3495,28 +3500,27 @@ const AppCreator = (defaultprops) => {
|
|||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const HandleIndividualChip = (props) => {
|
const HandleIndividualChip = (props) => {
|
||||||
const { chipData, index } = props;
|
const { chipData, index } = props;
|
||||||
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
||||||
|
|
||||||
const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData
|
const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||||
<Chip
|
<Chip
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||||||
height: 30,
|
height: 30,
|
||||||
margin: 3,
|
margin: 3,
|
||||||
paddingLeft: 5,
|
paddingLeft: 5,
|
||||||
paddingRight: 5,
|
paddingRight: 5,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
borderColor: theme.palette.chipStyle.borderColor,
|
borderColor: theme.palette.chipStyle.borderColor,
|
||||||
color: theme.palette.chipStyle.color,
|
color: theme.palette.chipStyle.color,
|
||||||
}}
|
}}
|
||||||
label={parsedChip}
|
label={parsedChip}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chipRequired) {
|
if (chipRequired) {
|
||||||
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
|
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
|
||||||
} else {
|
} else {
|
||||||
@@ -3524,27 +3528,28 @@ const AppCreator = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentAction(currentAction);
|
setCurrentAction(currentAction);
|
||||||
setChipRequired(!chipRequired);
|
setChipRequired(!chipRequired);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const setActionField = (field, value) => {
|
|
||||||
currentAction[field] = value
|
|
||||||
setCurrentAction(currentAction)
|
|
||||||
|
|
||||||
//setUrlPathQueries(currentAction.queries)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPathQuery = () => {
|
const setActionField = (field, value) => {
|
||||||
|
currentAction[field] = value
|
||||||
|
setCurrentAction(currentAction)
|
||||||
|
|
||||||
|
//setUrlPathQueries(currentAction.queries)
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPathQuery = () => {
|
||||||
urlPathQueries.push({ name: "", required: true, example: "", });
|
urlPathQueries.push({ name: "", required: true, example: "", });
|
||||||
if (updater === "addupdater") {
|
if (updater === "addupdater") {
|
||||||
setUpdater("updater");
|
setUpdater("updater");
|
||||||
} else {
|
} else {
|
||||||
setUpdater("addupdater");
|
setUpdater("addupdater");
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrlPathQueries(urlPathQueries);
|
setUrlPathQueries(urlPathQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3555,6 +3560,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
} else {
|
} else {
|
||||||
setUpdater("flipupdater");
|
setUpdater("flipupdater");
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrlPathQueries(urlPathQueries);
|
setUrlPathQueries(urlPathQueries);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3573,7 +3579,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||||||
<div>
|
<div>
|
||||||
<Divider
|
<Divider
|
||||||
style={{
|
style={{
|
||||||
@@ -3591,51 +3597,42 @@ const AppCreator = (defaultprops) => {
|
|||||||
return (
|
return (
|
||||||
<Paper key={queryIndex} style={actionListStyle}>
|
<Paper key={queryIndex} style={actionListStyle}>
|
||||||
<div style={{ marginLeft: "5px", width: "100%" }}>
|
<div style={{ marginLeft: "5px", width: "100%" }}>
|
||||||
<div style={{display: "flex"}}>
|
<div style={{display: "flex"}}>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
defaultValue={query.name}
|
defaultValue={query.name}
|
||||||
placeholder={"Query name (key)"}
|
placeholder={"Query name (key)"}
|
||||||
label={"Query Key"}
|
label={"Query Key"}
|
||||||
helperText={
|
onBlur={(e) => {
|
||||||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "")
|
||||||
Click required to flip
|
setUrlPathQueries(urlPathQueries)
|
||||||
</span>
|
}}
|
||||||
}
|
style={{flex: 3}}
|
||||||
onBlur={(e) => {
|
InputProps={{
|
||||||
console.log("IN BLUR: ", e.target.value);
|
style: {
|
||||||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "");
|
color: theme.palette.text.primary,
|
||||||
setUrlPathQueries(urlPathQueries);
|
},
|
||||||
}}
|
}}
|
||||||
style={{flex: 3}}
|
/>
|
||||||
InputProps={{
|
<TextField
|
||||||
style: {
|
fullWidth={true}
|
||||||
color: theme.palette.text.primary,
|
defaultValue={query.example}
|
||||||
},
|
placeholder={"Default value"}
|
||||||
}}
|
label={"Example"}
|
||||||
/>
|
onBlur={(e) => {
|
||||||
<TextField
|
// E.g. for Jira -> JQL -> requires = in param
|
||||||
fullWidth={true}
|
urlPathQueries[queryIndex].example = e.target.value.replaceAll("=","=")
|
||||||
defaultValue={query.example}
|
setUrlPathQueries(urlPathQueries)
|
||||||
placeholder={"Default value"}
|
}}
|
||||||
label={"Example"}
|
style={{flex: 2}}
|
||||||
onBlur={(e) => {
|
InputProps={{
|
||||||
urlPathQueries[queryIndex].example = e.target.value.replaceAll(
|
style: {
|
||||||
"=",
|
color: theme.palette.text.primary,
|
||||||
""
|
},
|
||||||
)
|
}}
|
||||||
|
/>
|
||||||
setUrlPathQueries(urlPathQueries)
|
</div>
|
||||||
}}
|
|
||||||
style={{flex: 2}}
|
|
||||||
InputProps={{
|
|
||||||
style: {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -3654,7 +3651,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
deletePathQuery(queryIndex);
|
deletePathQuery(queryIndex);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
@@ -4106,22 +4103,22 @@ const AppCreator = (defaultprops) => {
|
|||||||
if (request.header !== undefined && request.header !== null) {
|
if (request.header !== undefined && request.header !== null) {
|
||||||
var headers = [];
|
var headers = [];
|
||||||
for (let [key, value] of Object.entries(request.header)) {
|
for (let [key, value] of Object.entries(request.header)) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
if (key.includes(":")) {
|
if (key.includes(":")) {
|
||||||
const keysplit = key.split(":")
|
const keysplit = key.split(":")
|
||||||
key = keysplit[0].trim()
|
key = keysplit[0].trim()
|
||||||
value = keysplit[1].trim()
|
value = keysplit[1].trim()
|
||||||
|
|
||||||
} else if (key.includes("=")) {
|
} else if (key.includes("=")) {
|
||||||
const keysplit = key.split("=")
|
const keysplit = key.split("=")
|
||||||
key = keysplit[0].trim()
|
key = keysplit[0].trim()
|
||||||
value = keysplit[1].trim()
|
value = keysplit[1].trim()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
toast("Removed key: ", key)
|
toast("Removed key: ", key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parameterName !== undefined &&
|
parameterName !== undefined &&
|
||||||
@@ -4392,9 +4389,8 @@ const AppCreator = (defaultprops) => {
|
|||||||
variant={urlPath.length > 0 ? "contained" : "outlined"}
|
variant={urlPath.length > 0 ? "contained" : "outlined"}
|
||||||
style={{ }}
|
style={{ }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
//console.log(urlPathQueries)
|
|
||||||
//console.log(urlPath)
|
|
||||||
console.log(currentAction);
|
console.log(currentAction);
|
||||||
|
|
||||||
const errors = getActionErrors();
|
const errors = getActionErrors();
|
||||||
addActionToView(errors);
|
addActionToView(errors);
|
||||||
setActionsModalOpen(false);
|
setActionsModalOpen(false);
|
||||||
@@ -4460,7 +4456,7 @@ const AppCreator = (defaultprops) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper key={index} style={actionListStyle}>
|
<Paper key={index} style={actionListStyle}>
|
||||||
{newActionModal}
|
{newActionModal}
|
||||||
|
|
||||||
{error}
|
{error}
|
||||||
<Tooltip title="Edit action" placement="bottom">
|
<Tooltip title="Edit action" placement="bottom">
|
||||||
|
|||||||
@@ -3075,7 +3075,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
|||||||
|
|
||||||
const appEnding = app?.public === true ? app?.app_version : app?.id
|
const appEnding = app?.public === true ? app?.app_version : app?.id
|
||||||
|
|
||||||
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip`
|
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip${!app?.public ? ` \\\n docker tag frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding} frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${app.app_version}` : ``}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedActionOptions = deduplicateByName((
|
const renderedActionOptions = deduplicateByName((
|
||||||
@@ -3405,7 +3405,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
|||||||
<div style={{ textAlign: "center", marginTop: 25 }}>
|
<div style={{ textAlign: "center", marginTop: 25 }}>
|
||||||
<Link
|
<Link
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}`}
|
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`}
|
||||||
style={{ textDecoration: "none" }}
|
style={{ textDecoration: "none" }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -4300,8 +4300,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
|||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
//navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
|
navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
|
||||||
toast("You must be logged in to activate apps! Go to /login first.")
|
// toast("You must be logged in to activate apps! Go to /login first.")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { debounce } from "lodash";
|
|||||||
import AppSelection from "../components/AppSelection.jsx";
|
import AppSelection from "../components/AppSelection.jsx";
|
||||||
import AppModal from "../components/AppModal.jsx";
|
import AppModal from "../components/AppModal.jsx";
|
||||||
import AppCreationModal from "../components/AppCreationModal.jsx";
|
import AppCreationModal from "../components/AppCreationModal.jsx";
|
||||||
|
import Dropzone from "../components/Dropzone.jsx";
|
||||||
|
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
@@ -1136,6 +1137,7 @@ const Apps2 = (props) => {
|
|||||||
const [field2, setField2] = useState("");
|
const [field2, setField2] = useState("");
|
||||||
const [validation, setValidation] = useState(null);
|
const [validation, setValidation] = useState(null);
|
||||||
const [createAppModalOpen, setCreateAppModalOpen] = useState(false);
|
const [createAppModalOpen, setCreateAppModalOpen] = useState(false);
|
||||||
|
const [openApiData, setOpenApiData] = useState("");
|
||||||
|
|
||||||
const {themeMode, brandColor} = useContext(Context);
|
const {themeMode, brandColor} = useContext(Context);
|
||||||
const theme = getTheme(themeMode, brandColor);
|
const theme = getTheme(themeMode, brandColor);
|
||||||
@@ -1736,6 +1738,31 @@ const Apps2 = (props) => {
|
|||||||
// setOpenModal(true);
|
// setOpenModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadFile = (e) => {
|
||||||
|
const isFromDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
|
||||||
|
const files = isFromDropzone ? e.dataTransfer.files : e.target.files;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader.addEventListener("load", (ev) => {
|
||||||
|
const content = ev.target.result;
|
||||||
|
setOpenApiData(content);
|
||||||
|
setCreateAppModalOpen(true);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error in dropzone: ", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader.readAsText(files[0]);
|
||||||
|
} catch (error) {
|
||||||
|
toast("Failed to read file");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation and redirect are handled inside AppCreationModal
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const apps = currTab === 1 ? userApps : orgApps;
|
const apps = currTab === 1 ? userApps : orgApps;
|
||||||
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
|
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
|
||||||
@@ -1853,6 +1880,10 @@ const Apps2 = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dropzone
|
||||||
|
style={{ width: "100%", height: "100vh" }}
|
||||||
|
onDrop={uploadFile}
|
||||||
|
>
|
||||||
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
|
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
|
||||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||||
<AppModal
|
<AppModal
|
||||||
@@ -1869,6 +1900,8 @@ const Apps2 = (props) => {
|
|||||||
theme={theme}
|
theme={theme}
|
||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
|
startOpenApi={openApiData?.length > 0}
|
||||||
|
prefillOpenApiData={openApiData}
|
||||||
/>
|
/>
|
||||||
{appsModalLoad}
|
{appsModalLoad}
|
||||||
<div style={boxStyle}>
|
<div style={boxStyle}>
|
||||||
@@ -2198,6 +2231,24 @@ const Apps2 = (props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
title="Create an app with different options or Just drop a YAML/JSON file here"
|
||||||
|
placement="top"
|
||||||
|
componentsProps={{
|
||||||
|
tooltip: {
|
||||||
|
sx: {
|
||||||
|
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||||
|
color: "rgba(241, 241, 241, 1)",
|
||||||
|
fontSize: 12,
|
||||||
|
width: 240,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
border: "1px solid rgba(73, 73, 73, 1)",
|
||||||
|
fontFamily: theme?.typography?.fontFamily,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: "25%",
|
width: "25%",
|
||||||
minWidth: "25%",
|
minWidth: "25%",
|
||||||
@@ -2222,6 +2273,7 @@ const Apps2 = (props) => {
|
|||||||
Create an App
|
Create an App
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
@@ -2365,6 +2417,7 @@ const Apps2 = (props) => {
|
|||||||
<Configure clickAnalytics />
|
<Configure clickAnalytics />
|
||||||
</InstantSearch>
|
</InstantSearch>
|
||||||
</div>
|
</div>
|
||||||
|
</Dropzone>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
import { ToastContainer, toast } from "react-toastify"
|
import { ToastContainer, toast } from "react-toastify"
|
||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
import { LoadStats } from '../components/LineChartWrapper.jsx';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
@@ -828,9 +828,14 @@ const Dashboard = (props) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DashboardBarchart
|
<LineChartWrapper
|
||||||
timelineData={data}
|
inputname={"heyo"}
|
||||||
height={50}
|
keys={data}
|
||||||
|
height={100}
|
||||||
|
width={100}
|
||||||
|
border={false}
|
||||||
|
|
||||||
|
color={"#808080"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -400,6 +400,10 @@ const Docs = (defaultprops) => {
|
|||||||
if (propkey === "app_creation") {
|
if (propkey === "app_creation") {
|
||||||
navigate('/docs/apps#app-creation-introduction')
|
navigate('/docs/apps#app-creation-introduction')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (propkey === "api") {
|
||||||
|
navigate('/docs/API')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -690,7 +694,8 @@ const Docs = (defaultprops) => {
|
|||||||
|
|
||||||
const Heading = (props) => {
|
const Heading = (props) => {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
var id = props.children[0].toLowerCase().toString()
|
|
||||||
|
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||||
if (props.level <= 3) {
|
if (props.level <= 3) {
|
||||||
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ const LoginPage = props => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.pathname = "/workflows"
|
window.location.pathname = "/new-dashboard"
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ const LoginPage = props => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.pathname = "/workflows"
|
window.location.pathname = "/new-dashboard"
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import React, { useEffect, useState, useContext, useRef, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Tooltip,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
TrendingFlat as TrendingFlatIcon,
|
||||||
|
TrendingUp as TrendingUpIcon,
|
||||||
|
TrendingDown as TrendingDownIcon,
|
||||||
|
TaskAlt as TaskAltIcon,
|
||||||
|
SuccessFailed as SuccessFailedIcon,
|
||||||
|
RunsOverTime as RunsOverTimeIcon,
|
||||||
|
ErrorOutline as ErrorOutlineIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
import SuccessFailedRunsWidget from '../components/SuccessFailedRunsWidget.jsx';
|
||||||
|
import RunsOverTimeWidget from '../components/RunsOverTimeWidget.jsx';
|
||||||
|
import DashboardOnboarding from '../components/DashboardOnboarding.jsx';
|
||||||
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const NewDashboard = (props) => {
|
||||||
|
const { globalUrl, serverside, userdata } = props;
|
||||||
|
|
||||||
|
const { leftSideBarOpenByClick } = useContext(Context);
|
||||||
|
const [sfwControls, setSfwControls] = useState(null);
|
||||||
|
const [loadingSfw, setLoadingSfw] = useState(true);
|
||||||
|
const [loadingRot, setLoadingRot] = useState(true);
|
||||||
|
const [loadingNoti, setLoadingNoti] = useState(true);
|
||||||
|
const [showOverlay, setShowOverlay] = useState(true);
|
||||||
|
const [totals, setTotals] = useState({ days: 30, mode: 'workflows', totalRuns: 0, successRuns: 0, failedRuns: 0, activeDays: 0, timeSavedMinutes: 0, moneySavedDollars: 0 });
|
||||||
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [onboardingOpen, setOnboardingOpen] = useState(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem("dashboard_onboarding_complete") === "true" ? false : true;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
const [overrideDays, setOverrideDays] = useState(undefined);
|
||||||
|
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
|
||||||
|
const [isProdStatusOn, setIsProdStatusOn] = useState(false)
|
||||||
|
|
||||||
|
const isCloud =
|
||||||
|
serverside === true || typeof window === "undefined"
|
||||||
|
? true
|
||||||
|
: window.location.host === "localhost:3002" ||
|
||||||
|
window.location.host === "shuffler.io" ||
|
||||||
|
window.location.host === "localhost:5002";
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleSfwControls = useCallback((node) => {
|
||||||
|
setSfwControls(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatCurrencyCompact = (value) => {
|
||||||
|
const n = Math.max(0, Number(value) || 0);
|
||||||
|
const abs = Math.abs(n);
|
||||||
|
const fmt = (x, suffix) => `${(Math.round(x * 10) / 10).toString().replace(/\.0$/, '')}${suffix}`;
|
||||||
|
if (abs >= 1e9) return `$${fmt(n / 1e9, 'B')}`;
|
||||||
|
if (abs >= 1e6) return `$${fmt(n / 1e6, 'M')}`;
|
||||||
|
if (abs >= 1e3) return `$${fmt(n / 1e3, 'k')}`;
|
||||||
|
return `$${Math.round(n).toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeDisplay = (mins) => {
|
||||||
|
const totalMins = Math.max(0, Math.round(mins || 0));
|
||||||
|
if (totalMins < 60) return { display: `${totalMins}m`, title: `${totalMins} minutes` };
|
||||||
|
const totalHours = Math.floor(totalMins / 60);
|
||||||
|
if (totalHours >= 24) {
|
||||||
|
const days = Math.floor(totalHours / 24);
|
||||||
|
return { display: `${days}d`, title: `${totalHours} hours` };
|
||||||
|
}
|
||||||
|
return { display: `${totalHours}h`, title: `${totalHours} hours` };
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeFmt = formatTimeDisplay(totals.timeSavedMinutes);
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => n && n.read === false).length;
|
||||||
|
const readCount = notifications.filter(n => n && n.read === true).length;
|
||||||
|
|
||||||
|
// Current values
|
||||||
|
// 1 Workflow run = 15 minutes
|
||||||
|
// 1 Workflow run = $25
|
||||||
|
|
||||||
|
const STATIC_TIME_PERCENT = 'TBD'
|
||||||
|
const STATIC_MONEY_PERCENT = 'TBD'
|
||||||
|
const kpis = [
|
||||||
|
{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true},
|
||||||
|
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, },
|
||||||
|
{ value: String(unreadCount), label: 'Total errors', icon: <ErrorOutlineIcon sx={{ color: '#f87171', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#f87171' },
|
||||||
|
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#5cc879' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getGreeting = () => {
|
||||||
|
try {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour < 5) return 'Good night';
|
||||||
|
if (hour < 12) return 'Good morning';
|
||||||
|
if (hour < 18) return 'Good afternoon';
|
||||||
|
return 'Good evening';
|
||||||
|
} catch {
|
||||||
|
return 'Hey';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Auto-open onboarding when there aren't enough active days of stats
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const alreadyDone = localStorage.getItem("dashboard_onboarding_complete") === "true";
|
||||||
|
if (alreadyDone) {
|
||||||
|
setOnboardingOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const active = Number(totals?.activeDays || 0);
|
||||||
|
setOnboardingOpen(active < 5);
|
||||||
|
} catch {
|
||||||
|
setOnboardingOpen(true);
|
||||||
|
}
|
||||||
|
}, [totals?.activeDays]);
|
||||||
|
|
||||||
|
// Load notifications
|
||||||
|
useEffect(() => {
|
||||||
|
const loadNotifications = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${globalUrl}/api/v1/notifications`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
setNotifications([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const list = Array.isArray(data?.notifications) ? data.notifications : (Array.isArray(data) ? data : []);
|
||||||
|
setNotifications(list.filter(Boolean));
|
||||||
|
} catch (e) {
|
||||||
|
setNotifications([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingNoti(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadNotifications();
|
||||||
|
}, [globalUrl]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // Lightweight workflows list for selector in success/failed widget
|
||||||
|
// const loadWorkflows = async () => {
|
||||||
|
// try {
|
||||||
|
// const resp = await fetch(`${globalUrl}/api/v1/workflows`, {
|
||||||
|
// method: 'GET',
|
||||||
|
// credentials: 'include',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// });
|
||||||
|
// if (resp.status !== 200) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const data = await resp.json();
|
||||||
|
// const list = Array.isArray(data?.workflows) ? data.workflows : (Array.isArray(data) ? data : []);
|
||||||
|
// const normalized = list.filter(Boolean).map((w, idx) => ({ id: w?.id || w?.ID || `${idx}`, name: w?.name || w?.Name || `Workflow ${idx+1}` }));
|
||||||
|
// setWorkflows(normalized);
|
||||||
|
// } catch (e) {
|
||||||
|
// // ignore
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// loadWorkflows();
|
||||||
|
// }, [globalUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const orgId = userdata?.active_org?.id;
|
||||||
|
if (!orgId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetched = false;
|
||||||
|
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((response) => (response.ok ? response.json() : null))
|
||||||
|
.then((org) => {
|
||||||
|
if (!fetched && org) {
|
||||||
|
if (!isCloud) {
|
||||||
|
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||||
|
setIsProdStatusOn(true);
|
||||||
|
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||||
|
setIsProdStatusOn(true);
|
||||||
|
} else {
|
||||||
|
setIsProdStatusOn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
fetched = true;
|
||||||
|
};
|
||||||
|
}, [userdata?.active_org?.id, globalUrl]);
|
||||||
|
|
||||||
|
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' }}>
|
||||||
|
<DashboardOnboarding
|
||||||
|
open={onboardingOpen}
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
onClose={() => {
|
||||||
|
setOnboardingOpen(false)
|
||||||
|
}}
|
||||||
|
setOnboardingOpen={setOnboardingOpen}
|
||||||
|
isProdStatusOn={isProdStatusOn}
|
||||||
|
isCloud={isCloud}
|
||||||
|
onExplore={() => {
|
||||||
|
// Ensure overrides are set before closing modal
|
||||||
|
setOverrideDays(5);
|
||||||
|
setRotMonthOverride(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||||
|
|
||||||
|
// Close modal immediately to trigger data fetching
|
||||||
|
setOnboardingOpen(false);
|
||||||
|
}}
|
||||||
|
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 }}>
|
||||||
|
<CircularProgress size={36} thickness={4} />
|
||||||
|
<Typography variant="body2" color="textSecondary">Loading dashboard…</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Header / Greeting */}
|
||||||
|
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
||||||
|
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
||||||
|
<>
|
||||||
|
{sfwControls}
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* KPI cards */}
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{kpis.map((kpi) => (
|
||||||
|
<Grid item xs={12} sm={6} md={3} key={kpi.label}>
|
||||||
|
<Tooltip title={kpi.disabled ? "This metric is coming soon!" : ""} arrow>
|
||||||
|
<Paper style={{ padding: 16, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12
|
||||||
|
, cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default',
|
||||||
|
transparency: kpi.disabled ? 0.5 : 1
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (kpi.label.toLowerCase().includes('total errors')) {
|
||||||
|
// navigate to notifications page
|
||||||
|
navigate('/admin?admin_tab=notifications');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||||
|
<Stack sx={{py:2, paddingLeft: 1}}>
|
||||||
|
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
|
||||||
|
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
|
||||||
|
{kpi.icon}
|
||||||
|
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Tooltip>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Success/Failed widget uses its own internal sub-cards; make wrapper transparent */}
|
||||||
|
<Paper elevation={0} style={{ padding: 0, marginTop: 5, background: 'transparent', boxShadow: 'none', border: 'none' }}>
|
||||||
|
<SuccessFailedRunsWidget
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
overrideDays={overrideDays}
|
||||||
|
dummyMode={onboardingOpen}
|
||||||
|
onControlsChange={handleSfwControls}
|
||||||
|
onLoadingChange={setLoadingSfw}
|
||||||
|
onTotalsChange={setTotals}
|
||||||
|
/>
|
||||||
|
</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} />
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewDashboard;
|
||||||
|
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
const [executionLoading, setExecutionLoading] = useState(false);
|
const [executionLoading, setExecutionLoading] = useState(false);
|
||||||
const [executionData, setExecutionData] = React.useState({});
|
const [executionData, setExecutionData] = React.useState({});
|
||||||
const [executionRunning, setExecutionRunning] = useState(false);
|
const [executionRunning, setExecutionRunning] = useState(false);
|
||||||
|
const [disableButtons, setDisableButtons] = useState(false);
|
||||||
const [workflowQuestion, setWorkflowQuestion] = useState("");
|
const [workflowQuestion, setWorkflowQuestion] = useState("");
|
||||||
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
|
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
|
||||||
const [apps, setApps] = React.useState([]);
|
const [apps, setApps] = React.useState([]);
|
||||||
@@ -84,12 +85,14 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
const [workflows, setWorkflows] = React.useState([])
|
const [workflows, setWorkflows] = React.useState([])
|
||||||
const [boxWidth, setBoxWidth] = React.useState(500)
|
const [boxWidth, setBoxWidth] = React.useState(500)
|
||||||
const [inputQuestions, setInputQuestions] = React.useState([])
|
const [inputQuestions, setInputQuestions] = React.useState([])
|
||||||
|
const [agentic, setAgentic] = React.useState(false)
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search)
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
const answer = searchParams.get("answer")
|
const answer = searchParams.get("answer")
|
||||||
const execution_id = searchParams.get("reference_execution")
|
const execution_id = searchParams.get("reference_execution")
|
||||||
const authorization = searchParams.get("authorization")
|
const authorization = searchParams.get("authorization")
|
||||||
const sourceNode = searchParams.get("source_node")
|
const sourceNode = searchParams.get("source_node")
|
||||||
|
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||||
const backendUrl = searchParams.get("backend_url") || globalUrl
|
const backendUrl = searchParams.get("backend_url") || globalUrl
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -162,11 +165,8 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to swap from login to register. True = login, false = register
|
|
||||||
|
|
||||||
// Error messages etc
|
// Error messages etc
|
||||||
const [executionInfo, setExecutionInfo] = useState("");
|
const [executionInfo, setExecutionInfo] = useState("");
|
||||||
|
|
||||||
const handleValidateForm = (executionArgument) => {
|
const handleValidateForm = (executionArgument) => {
|
||||||
// Check if every field exists
|
// Check if every field exists
|
||||||
if (executionArgument === undefined || executionArgument === null) {
|
if (executionArgument === undefined || executionArgument === null) {
|
||||||
@@ -184,9 +184,12 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("EXEC: ", executionArgument)
|
// FIXME: Error with User Input + Required arg (?)
|
||||||
|
// Somehow validation is not happening as it should, and it just checks all
|
||||||
|
// questions if none are selected
|
||||||
for (var key in executionArgument) {
|
for (var key in executionArgument) {
|
||||||
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
|
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
|
||||||
|
console.log("Unanswered, required question: ", key)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,17 +337,18 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validate = validateJson(executionData.result)
|
const validate = validateJson(executionData.result)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{marginTop: executionMargin, }}>
|
<div style={{marginTop: executionMargin, }}>
|
||||||
{workflowQuestion !== "" ? null :
|
{workflowQuestion !== "" ? null :
|
||||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
<div style={{marginTop: 20, marginBottom: 20, }}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{workflowQuestion !== "" ? null :
|
{workflowQuestion !== "" ? null :
|
||||||
validate.valid === false ?
|
validate.valid === false ?
|
||||||
<div style={{marginTop: 20, }}>
|
<div style={{marginTop: 20, }}>
|
||||||
<Divider />
|
{validate?.result !== undefined && validate?.result !== null && validate?.result.length > 0 ?
|
||||||
|
<Divider />
|
||||||
|
: null }
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
img: Img,
|
img: Img,
|
||||||
@@ -397,10 +401,13 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
stop()
|
stop()
|
||||||
setMessage("")
|
setMessage("")
|
||||||
setExecutionLoading(true)
|
|
||||||
setExecutionData({})
|
setExecutionData({})
|
||||||
setExecutionInfo("")
|
setExecutionInfo("")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setExecutionLoading(true)
|
||||||
|
}, 2500)
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
"execution_argument": executionArgument,
|
"execution_argument": executionArgument,
|
||||||
"execution_source": "form",
|
"execution_source": "form",
|
||||||
@@ -462,6 +469,14 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
fetchBody.body = JSON.stringify(data)
|
fetchBody.body = JSON.stringify(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (agentic === true) {
|
||||||
|
if (url.includes("?")) {
|
||||||
|
url += `&agentic=true&decision_id=${decisionId}`
|
||||||
|
} else {
|
||||||
|
url += `?agentic=true&decision_id=${decisionId}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IF there is an execution argument, we should use it
|
// IF there is an execution argument, we should use it
|
||||||
fetch(url, fetchBody)
|
fetch(url, fetchBody)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -480,25 +495,30 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) {
|
//if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization?.length === 0) {
|
||||||
toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form`)
|
// toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form (2)`)
|
||||||
}
|
//}
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
})
|
})
|
||||||
.then(responseJson => {
|
.then(responseJson => {
|
||||||
|
//if (responseJson.success === true) {
|
||||||
|
// setDisableButtons(true)
|
||||||
|
//}
|
||||||
|
|
||||||
setExecutionLoading(false)
|
setExecutionLoading(false)
|
||||||
if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) {
|
if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0) {
|
||||||
navigate(`?execution_id=${responseJson.execution_id}`)
|
navigate(`?execution_id=${responseJson.execution_id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseJson.success === false) {
|
if (responseJson.success === false) {
|
||||||
|
|
||||||
console.log("Failed sending execution request")
|
console.log("Failed sending execution request")
|
||||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
if (responseJson?.reason !== undefined && responseJson?.reason !== null) {
|
||||||
if (responseJson?.reason?.toLowerCase().includes("already clicked")) {
|
if (responseJson?.reason?.toLowerCase().includes("already clicked")) {
|
||||||
setMessage("Already answered. You may close this window (2).")
|
setMessage("This form has been answered. You may close this window.")
|
||||||
} else {
|
} else {
|
||||||
toast.warn(responseJson.reason)
|
toast.warn(responseJson?.reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,11 +540,17 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
setExecutionRequest(responseJson)
|
setExecutionRequest(responseJson)
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If execution_id or authorization, add them to the URL
|
||||||
|
if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0 && responseJson?.authorization !== undefined && responseJson?.authorization !== null && responseJson?.authorization?.length > 0) {
|
||||||
|
navigate(`?execution_id=${responseJson.execution_id}&authorization=${responseJson.authorization}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
//setExecutionInfo("Error in workflow startup: " + error)
|
//setExecutionInfo("Error in workflow startup: " + error)
|
||||||
toast.warn("Error submitting form. Please try again.")
|
console.log("Error starting workflow: ", error)
|
||||||
|
toast.warn(`Error submitting form. Please try again: ${error}`)
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
setMessage("")
|
setMessage("")
|
||||||
@@ -597,8 +623,8 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||||
const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) {
|
} else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
|
||||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,10 +634,10 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
console.log("Get workflow error: ", error.toString())
|
console.log("Get workflow error: ", error.toString())
|
||||||
|
|
||||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) {
|
} else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
|
||||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -646,6 +672,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
trig.parameters = []
|
trig.parameters = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newexec = {}
|
||||||
for (var paramkey in trig.parameters) {
|
for (var paramkey in trig.parameters) {
|
||||||
const param = trig.parameters[paramkey]
|
const param = trig.parameters[paramkey]
|
||||||
if (param.name !== "input_questions") {
|
if (param.name !== "input_questions") {
|
||||||
@@ -683,6 +710,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Setting exec arg: ", newexec)
|
||||||
setExecutionArgument(newexec)
|
setExecutionArgument(newexec)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,10 +761,10 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
setInputQuestions(workflow.input_questions)
|
setInputQuestions(workflow.input_questions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) {
|
if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
|
||||||
// Look for {{ uuid }} format, and try to run that workflow with their account
|
// Look for {{ uuid }} format, and try to run that workflow with their account
|
||||||
// This is a hack, but a fun one.
|
// This is a hack, but a fun one.
|
||||||
var newmarkdown = workflow.form_control.input_markdown.replace("", "")
|
var newmarkdown = workflow?.form_control?.input_markdown.replace("", "")
|
||||||
|
|
||||||
const uuidRegex = /{{\s[a-f0-9-]+\s}}/g
|
const uuidRegex = /{{\s[a-f0-9-]+\s}}/g
|
||||||
const found = newmarkdown.match(uuidRegex)
|
const found = newmarkdown.match(uuidRegex)
|
||||||
@@ -784,8 +812,8 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflow.status !== "WAITING") {
|
if (workflow.status === "EXECUTING" || workflow.status === "SUCCESS" || workflow.status === "ABORTED" || workflow.status === "STOPPED" || workflow.status === "FAILURE" || workflow.status === "FINISHED") {
|
||||||
setMessage("Already answered. You may close this window (3).")
|
setMessage("Already handled. You may close this window.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,13 +834,17 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
console.log("Status not 200 for workflows :O!");
|
console.log("Status not 200 for workflows :O!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) {
|
//if (response.status >= 400 && authorization === undefined || authorization === null || authorization.length === 0) {
|
||||||
toast(`This form is not available to you. If you think this is an error, please contact ${supportEmail} with the URL.`)
|
// toast.warn(`This form may not be available to you. If you think this is an error, please contact ${supportEmail} with the URL.`)
|
||||||
}
|
//}
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
})
|
})
|
||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
|
if (responseJson.success === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Not sure why this is necessary.
|
// Not sure why this is necessary.
|
||||||
if (responseJson.isValid === undefined) {
|
if (responseJson.isValid === undefined) {
|
||||||
responseJson.isValid = true;
|
responseJson.isValid = true;
|
||||||
@@ -1008,14 +1040,78 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
if (responseJson.success == false) {
|
if (responseJson?.success == false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 && disableButtons === false && responseJson?.status !== "" && responseJson?.status !== "WAITING") {
|
||||||
|
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) {
|
||||||
setupSourcenode(responseJson.workflow, sourceNode)
|
setupSourcenode(responseJson.workflow, sourceNode)
|
||||||
setWorkflow(responseJson.workflow)
|
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)
|
||||||
|
|
||||||
|
setAgentic(true)
|
||||||
|
|
||||||
|
for (var resultkey in responseJson.results) {
|
||||||
|
const result = responseJson.results[resultkey]
|
||||||
|
if (result.action.id !== sourceNode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const validated = validateJson(result.result)
|
||||||
|
if (!validated.valid) {
|
||||||
|
console.log("Error parsing result: ", validated.error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedresult = validated.result
|
||||||
|
console.log("PARSED RES: ", parsedresult)
|
||||||
|
if (parsedresult?.decisions?.length > 0) {
|
||||||
|
var newexec = executionArgument
|
||||||
|
if (newexec === undefined || newexec === null || Object.keys(newexec).length === 0) {
|
||||||
|
newexec = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var decisionkey in parsedresult?.decisions) {
|
||||||
|
const decision = parsedresult.decisions[decisionkey]
|
||||||
|
if (decision?.run_details?.id !== decisionId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var fieldkey in decision?.fields) {
|
||||||
|
const field = decision.fields[fieldkey]
|
||||||
|
if (field.key === "question" && !inputQuestions.find(q => q.name=== field.value)) {
|
||||||
|
console.log("QUESTION: ", field)
|
||||||
|
const newquestion = {
|
||||||
|
"name": field.value,
|
||||||
|
"value": field.key+"_"+fieldkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
inputQuestions.push(newquestion)
|
||||||
|
|
||||||
|
newexec[newquestion.value] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputQuestions([...inputQuestions] )
|
||||||
|
console.log("EXEC: ", newexec)
|
||||||
|
setExecutionArgument(newexec)
|
||||||
|
|
||||||
|
responseJson.workflow.input_questions = inputQuestions
|
||||||
|
setWorkflow(responseJson?.workflow)
|
||||||
|
setDisableButtons(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1031,12 +1127,12 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
localStorage.setItem(storageKey, JSON.stringify(value))
|
localStorage.setItem(storageKey, JSON.stringify(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown?.length > 0) {
|
||||||
const newmarkdown = realtimeMarkdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
const newmarkdown = realtimeMarkdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
|
|
||||||
} else if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) {
|
} else if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
|
||||||
const newmarkdown = workflow.form_control.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
const newmarkdown = workflow?.form_control?.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
||||||
setRealtimeMarkdown(newmarkdown)
|
setRealtimeMarkdown(newmarkdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,7 +1168,6 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
getWorkflow(props.match.params.key, sourceNode)
|
getWorkflow(props.match.params.key, sourceNode)
|
||||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
|
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
|
||||||
console.log("Get execution: ", execution_id)
|
|
||||||
fetchUpdates(execution_id, authorization, true)
|
fetchUpdates(execution_id, authorization, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,13 +1231,13 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"}
|
const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"}
|
||||||
|
|
||||||
// Check if all fields are filled in?
|
// Check if all fields are filled in?
|
||||||
var disabledButtons = executionLoading || executionRunning || message.length > 0
|
var disabledButtons = executionLoading || executionRunning || message.length > 0 || disableButtons
|
||||||
if (disabledButtons === false && workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) {
|
if (disabledButtons === false && workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) {
|
||||||
// Check field values
|
// Check field values
|
||||||
//disabledButtons = handleValidateForm(executionArgument)
|
//disabledButtons = handleValidateForm(executionArgument)
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : "Unknown"
|
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : ""
|
||||||
const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io"
|
const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io"
|
||||||
//const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io"
|
//const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io"
|
||||||
|
|
||||||
@@ -1321,12 +1416,12 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
<div style={{paddingTop: 150, marginTop: 150, width: 250, itemAlign: "center", textAlign: "center", margin: "auto", }}>
|
<div style={{paddingTop: 150, marginTop: 150, width: 250, itemAlign: "center", textAlign: "center", margin: "auto", }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
<Typography variant="body1" style={{marginTop: 20, }}>
|
<Typography variant="body1" style={{marginTop: 20, }}>
|
||||||
Loading Form Details...
|
Loading Details...
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ?
|
{workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ?
|
||||||
<div style={{marginBottom: 20, }}>
|
<div style={{marginBottom: 20, }}>
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
@@ -1342,13 +1437,13 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
}}
|
}}
|
||||||
rehypePlugins={[rehypeRaw]}
|
rehypePlugins={[rehypeRaw]}
|
||||||
>
|
>
|
||||||
{workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow.form_control.input_markdown}
|
{workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow?.form_control?.input_markdown}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<form onSubmit={(e) => {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}>
|
<form onSubmit={(e) => {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}>
|
||||||
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ? null :
|
{workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ? null :
|
||||||
<div>
|
<div>
|
||||||
{/*
|
{/*
|
||||||
<img
|
<img
|
||||||
@@ -1370,10 +1465,12 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
|
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
|
||||||
{organization}
|
{organization}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
{organization?.length > 0 &&
|
||||||
|
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||||
|
}
|
||||||
|
|
||||||
{disabledButtons && message.length > 0 ? null :
|
{disabledButtons && message.length > 0 ? null :
|
||||||
<Typography color="textSecondary" style={{textAlign: "center", }}>
|
<Typography color="textSecondary" style={{textAlign: "center", marginTop: 15, }}>
|
||||||
{message}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
@@ -1412,6 +1509,11 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1]
|
executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parsedLabel = question?.value?.startsWith("question_") ?
|
||||||
|
""
|
||||||
|
:
|
||||||
|
question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{marginBottom: 10}} key={index}>
|
<div style={{marginBottom: 10}} key={index}>
|
||||||
|
|
||||||
@@ -1457,7 +1559,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
backgroundColor: theme.palette.inputColor,
|
backgroundColor: theme.palette.inputColor,
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
}}
|
}}
|
||||||
label={question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)}
|
label={parsedLabel}
|
||||||
required
|
required
|
||||||
|
|
||||||
disabled={disabledButtons}
|
disabled={disabledButtons}
|
||||||
@@ -1542,7 +1644,7 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
:
|
:
|
||||||
<Fade in={true} timeout={2500}>
|
<Fade in={true} timeout={2500}>
|
||||||
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
|
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
|
||||||
{disabledButtons ? "Already answered. You may close this window." : ""}
|
{disabledButtons ? "Question answered. You may close this window." : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Fade>
|
</Fade>
|
||||||
}
|
}
|
||||||
@@ -1565,10 +1667,13 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setButtonClicked("FINISHED")
|
// Timeout 2500 just in case
|
||||||
setExecutionData({
|
setTimeout(() => {
|
||||||
status: "FINISHED",
|
setButtonClicked("FINISHED")
|
||||||
})
|
setExecutionData({
|
||||||
|
status: "FINISHED",
|
||||||
|
})
|
||||||
|
}, 2500)
|
||||||
|
|
||||||
onSubmit(null, execution_id, authorization, true)
|
onSubmit(null, execution_id, authorization, true)
|
||||||
}}>
|
}}>
|
||||||
@@ -1586,16 +1691,24 @@ const RunWorkflow = (defaultprops) => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
}} onClick={() => {
|
}} onClick={() => {
|
||||||
setButtonClicked("ABORTED")
|
setTimeout(() => {
|
||||||
setExecutionData({
|
setButtonClicked("ABORTED")
|
||||||
status: "ABORTED",
|
setExecutionData({
|
||||||
})
|
status: "ABORTED",
|
||||||
|
})
|
||||||
|
}, 2500)
|
||||||
|
|
||||||
onSubmit(null, execution_id, authorization, false)
|
onSubmit(null, execution_id, authorization, false)
|
||||||
}}>
|
}}>
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{handleValidateForm(executionArgument) === false && disabledButtons === false ?
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}>
|
||||||
|
All required questions have not been answered yet.
|
||||||
|
</Typography>
|
||||||
|
: null}
|
||||||
</span>
|
</span>
|
||||||
:
|
:
|
||||||
<div style={{display: "flex", marginTop: "15px"}}>
|
<div style={{display: "flex", marginTop: "15px"}}>
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ export const HandleJsonCopy = (base, copy, base_node_name) => {
|
|||||||
//var newitem = JSON.parse(base);
|
//var newitem = JSON.parse(base);
|
||||||
var newitem = validateJson(base).result
|
var newitem = validateJson(base).result
|
||||||
|
|
||||||
var to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_");
|
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||||
for (let copykey in copy.namespace) {
|
for (let copykey in copy.namespace) {
|
||||||
if (copy.namespace[copykey].includes("Results for")) {
|
if (copy.namespace[copykey].includes("Results for")) {
|
||||||
continue;
|
continue;
|
||||||
@@ -742,7 +742,7 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
|
|||||||
const Workflows = (props) => {
|
const Workflows = (props) => {
|
||||||
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
|
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
|
||||||
|
|
||||||
document.title = "Shuffle - Workflows";
|
document.title = "Workflows - Shuffle";
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const classes = useStyles(theme)
|
const classes = useStyles(theme)
|
||||||
|
|||||||
+546
-331
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@ dependencies:
|
|||||||
version: ^2.23.0
|
version: ^2.23.0
|
||||||
repository: oci://registry-1.docker.io/bitnamicharts
|
repository: oci://registry-1.docker.io/bitnamicharts
|
||||||
- name: opensearch
|
- name: opensearch
|
||||||
version: ^1.3.0
|
version: 2.0.10
|
||||||
repository: oci://registry-1.docker.io/bitnamicharts
|
repository: https://charts.bitnami.com/bitnami
|
||||||
condition: opensearch.enabled
|
condition: opensearch.enabled
|
||||||
|
|||||||
@@ -26,23 +26,23 @@ data:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
# Shuffle worker resources
|
# Shuffle worker resources
|
||||||
{{- $workerResources := (.Values.worker.resources | default (include "common.resources.preset" (dict "type" .Values.worker.resourcesPreset)) | fromYaml) -}}
|
{{- $workerResources := (.Values.worker.resources | default (include "common.resources.preset" (dict "type" .Values.worker.resourcesPreset) | fromYaml)) -}}
|
||||||
{{- if $workerResources.requests.cpu }}
|
{{- if and $workerResources.requests $workerResources.requests.cpu }}
|
||||||
SHUFFLE_WORKER_CPU_REQUEST: {{ $workerResources.requests.cpu | quote }}
|
SHUFFLE_WORKER_CPU_REQUEST: {{ $workerResources.requests.cpu | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $workerResources.requests.memory}}
|
{{- if and $workerResources.requests $workerResources.requests.memory}}
|
||||||
SHUFFLE_WORKER_MEMORY_REQUEST: {{ $workerResources.requests.memory | quote }}
|
SHUFFLE_WORKER_MEMORY_REQUEST: {{ $workerResources.requests.memory | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if (index $workerResources.requests "ephemeral-storage") }}
|
{{- if and $workerResources.requests (index $workerResources.requests "ephemeral-storage") }}
|
||||||
SHUFFLE_WORKER_EPHEMERAL_STORAGE_REQUEST: {{ (index $workerResources.requests "ephemeral-storage") | quote }}
|
SHUFFLE_WORKER_EPHEMERAL_STORAGE_REQUEST: {{ (index $workerResources.requests "ephemeral-storage") | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $workerResources.limits.cpu }}
|
{{- if and $workerResources.limits $workerResources.limits.cpu }}
|
||||||
SHUFFLE_WORKER_CPU_LIMIT: {{ $workerResources.limits.cpu | quote }}
|
SHUFFLE_WORKER_CPU_LIMIT: {{ $workerResources.limits.cpu | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $workerResources.limits.memory}}
|
{{- if and $workerResources.limits $workerResources.limits.memory}}
|
||||||
SHUFFLE_WORKER_MEMORY_LIMIT: {{ $workerResources.limits.memory | quote }}
|
SHUFFLE_WORKER_MEMORY_LIMIT: {{ $workerResources.limits.memory | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if (index $workerResources.limits "ephemeral-storage") }}
|
{{- if and $workerResources.limits (index $workerResources.limits "ephemeral-storage") }}
|
||||||
SHUFFLE_WORKER_EPHEMERAL_STORAGE_LIMIT: {{ (index $workerResources.limits "ephemeral-storage") | quote }}
|
SHUFFLE_WORKER_EPHEMERAL_STORAGE_LIMIT: {{ (index $workerResources.limits "ephemeral-storage") | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
@@ -57,22 +57,22 @@ data:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
# Shuffle app resources
|
# Shuffle app resources
|
||||||
{{- $appResources := (.Values.app.resources | default (include "common.resources.preset" (dict "type" .Values.app.resourcesPreset)) | fromYaml) -}}
|
{{- $appResources := (.Values.app.resources | default (include "common.resources.preset" (dict "type" .Values.app.resourcesPreset) | fromYaml)) -}}
|
||||||
{{- if $appResources.requests.cpu }}
|
{{- if and $appResources.requests $appResources.requests.cpu }}
|
||||||
SHUFFLE_APP_CPU_REQUEST: {{ $appResources.requests.cpu | quote }}
|
SHUFFLE_APP_CPU_REQUEST: {{ $appResources.requests.cpu | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $appResources.requests.memory}}
|
{{- if and $appResources.requests $appResources.requests.memory }}
|
||||||
SHUFFLE_APP_MEMORY_REQUEST: {{ $appResources.requests.memory | quote }}
|
SHUFFLE_APP_MEMORY_REQUEST: {{ $appResources.requests.memory | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if (index $appResources.requests "ephemeral-storage") }}
|
{{- if and $appResources.requests (index $appResources.requests "ephemeral-storage") }}
|
||||||
SHUFFLE_APP_EPHEMERAL_STORAGE_REQUEST: {{ (index $appResources.requests "ephemeral-storage") | quote }}
|
SHUFFLE_APP_EPHEMERAL_STORAGE_REQUEST: {{ (index $appResources.requests "ephemeral-storage") | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $appResources.limits.cpu }}
|
{{- if and $appResources.limits $appResources.limits.cpu }}
|
||||||
SHUFFLE_APP_CPU_LIMIT: {{ $appResources.limits.cpu | quote }}
|
SHUFFLE_APP_CPU_LIMIT: {{ $appResources.limits.cpu | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $appResources.limits.memory}}
|
{{- if and $appResources.limits $appResources.limits.memory }}
|
||||||
SHUFFLE_APP_MEMORY_LIMIT: {{ $appResources.limits.memory | quote }}
|
SHUFFLE_APP_MEMORY_LIMIT: {{ $appResources.limits.memory | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if (index $appResources.limits "ephemeral-storage") }}
|
{{- if and $appResources.limits (index $appResources.limits "ephemeral-storage") }}
|
||||||
SHUFFLE_APP_EPHEMERAL_STORAGE_LIMIT: {{ (index $appResources.limits "ephemeral-storage") | quote }}
|
SHUFFLE_APP_EPHEMERAL_STORAGE_LIMIT: {{ (index $appResources.limits "ephemeral-storage") | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ diagnosticMode:
|
|||||||
shuffle:
|
shuffle:
|
||||||
## @param shuffle.baseUrl The external base URL under which Shuffle is reachable.
|
## @param shuffle.baseUrl The external base URL under which Shuffle is reachable.
|
||||||
##
|
##
|
||||||
baseUrl: ""
|
#baseUrl: ""
|
||||||
|
|
||||||
## ref: https://shuffler.io/docs/organizations
|
## ref: https://shuffler.io/docs/organizations
|
||||||
## This chart only supports single-tenant deployments at the moment
|
## This chart only supports single-tenant deployments at the moment
|
||||||
@@ -525,6 +525,8 @@ backend:
|
|||||||
## @param backend.openSearch.username The username that is used for authenticating with OpenSearch
|
## @param backend.openSearch.username The username that is used for authenticating with OpenSearch
|
||||||
##
|
##
|
||||||
username: admin
|
username: admin
|
||||||
|
|
||||||
|
password: StrongShufflePassword321!
|
||||||
## @param backend.openSearch.certificateFile The path to a custom OpenSearch certificate file
|
## @param backend.openSearch.certificateFile The path to a custom OpenSearch certificate file
|
||||||
##
|
##
|
||||||
certificateFile: ""
|
certificateFile: ""
|
||||||
@@ -966,7 +968,7 @@ orborus:
|
|||||||
image:
|
image:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
repository: shuffle/shuffle-orborus
|
repository: shuffle/shuffle-orborus
|
||||||
tag: ""
|
tag: "nightly"
|
||||||
digest: ""
|
digest: ""
|
||||||
## Specify a imagePullPolicy
|
## Specify a imagePullPolicy
|
||||||
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
|
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
|
||||||
@@ -1197,7 +1199,17 @@ orborus:
|
|||||||
## - name: FOO
|
## - name: FOO
|
||||||
## value: "bar"
|
## value: "bar"
|
||||||
##
|
##
|
||||||
extraEnvVars: []
|
extraEnvVars:
|
||||||
|
- name: SHUFFLE_APP_SDK_TIMEOUT
|
||||||
|
value: "300"
|
||||||
|
- name: SHUFFLE_ORBORUS_EXCUTION_CONCURRENCY
|
||||||
|
value: "7"
|
||||||
|
- name: SHUFFLE_STATS_DISABLED
|
||||||
|
value: "true"
|
||||||
|
- name: KUBERNETES_NAMESPACE
|
||||||
|
value: "shuffle"
|
||||||
|
- name: SHUFFLE_BASE_IMAGE_NAME
|
||||||
|
value: "frikky/shuffle"
|
||||||
## @param orborus.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for orborus containers
|
## @param orborus.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for orborus containers
|
||||||
##
|
##
|
||||||
extraEnvVarsCM: ""
|
extraEnvVarsCM: ""
|
||||||
@@ -1842,7 +1854,7 @@ volumePermissions:
|
|||||||
##
|
##
|
||||||
image:
|
image:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
repository: bitnami/os-shell
|
repository: bitnamilegacy/os-shell
|
||||||
tag: 12-debian-12-r30
|
tag: 12-debian-12-r30
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
## Optionally specify an array of imagePullSecrets.
|
## Optionally specify an array of imagePullSecrets.
|
||||||
@@ -1898,17 +1910,25 @@ volumePermissions:
|
|||||||
##
|
##
|
||||||
opensearch:
|
opensearch:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
sysctlImage:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: docker.io
|
||||||
|
repository: bitnamilegacy/opensearch
|
||||||
|
tag: "3.2.0"
|
||||||
|
|
||||||
master:
|
master:
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
data:
|
data:
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
coordinating:
|
coordinating:
|
||||||
replicaCount: 1
|
replicaCount: 0
|
||||||
ingest:
|
ingest:
|
||||||
replicaCount: 1
|
replicaCount: 0
|
||||||
dashboards:
|
dashboards:
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
## @section Vault Parameters
|
## @section Vault Parameters
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/docker/docker v28.3.3+incompatible
|
github.com/docker/docker v28.3.3+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/shuffle/shuffle-shared v0.9.15
|
github.com/shuffle/shuffle-shared v0.9.30
|
||||||
k8s.io/api v0.33.1
|
k8s.io/api v0.33.1
|
||||||
k8s.io/apimachinery v0.33.1
|
k8s.io/apimachinery v0.33.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -328,8 +328,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
|
|||||||
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
|
github.com/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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shuffle/shuffle-shared v0.9.15 h1:Gc7c0pbWG6nHWSTkcfAnKgQgWCWfc6aDQ/BIu20z6bM=
|
github.com/shuffle/shuffle-shared v0.9.30 h1:3CYvNyD7sTxdxoZjTVrtaDqFvSWQRKAFGaga6rPGf8A=
|
||||||
github.com/shuffle/shuffle-shared v0.9.15/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
|
github.com/shuffle/shuffle-shared v0.9.30/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
|||||||
+254
-135
@@ -12,7 +12,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"regexp"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
@@ -20,6 +19,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -52,6 +52,7 @@ import (
|
|||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
)
|
)
|
||||||
@@ -89,6 +90,7 @@ var debug = os.Getenv("DEBUG") == "true"
|
|||||||
// var baseimagename = "shuffle/shuffle"
|
// var baseimagename = "shuffle/shuffle"
|
||||||
var baseimageregistry = os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")
|
var baseimageregistry = os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")
|
||||||
var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
|
var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
|
||||||
|
|
||||||
//var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
|
//var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
|
||||||
|
|
||||||
// Used for cloud with auth. Onprem in certain cases too.
|
// Used for cloud with auth. Onprem in certain cases too.
|
||||||
@@ -116,7 +118,7 @@ var pipelineApikey = os.Getenv("SHUFFLE_PIPELINE_AUTH")
|
|||||||
var pipelineUrl = os.Getenv("SHUFFLE_PIPELINE_URL")
|
var pipelineUrl = os.Getenv("SHUFFLE_PIPELINE_URL")
|
||||||
|
|
||||||
var executionIds = []string{}
|
var executionIds = []string{}
|
||||||
var pipelines = []shuffle.PipelineInfoMini{}
|
var pipelines = []shuffle.PipelineInfo{}
|
||||||
var namespacemade = false // For K8s
|
var namespacemade = false // For K8s
|
||||||
var skipPipelineMount = false
|
var skipPipelineMount = false
|
||||||
var tenzirDisabled = false
|
var tenzirDisabled = false
|
||||||
@@ -343,7 +345,7 @@ func deployServiceWorkers(image string) {
|
|||||||
if len(dockerSwarmBridgeMTU) == 0 {
|
if len(dockerSwarmBridgeMTU) == 0 {
|
||||||
mtu, err = strconv.Atoi(dockerSwarmBridgeMTU) // by default
|
mtu, err = strconv.Atoi(dockerSwarmBridgeMTU) // by default
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[DEBUG] Failed to convert the default MTU to int: %s. Using 1500 instead. Input: %s", err, dockerSwarmBridgeMTU)
|
log.Printf("[DEBUG] Failed to convert the default MTU to int: %s. Using 1500 instead. Input: %s", err, dockerSwarmBridgeMTU)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,7 +532,6 @@ func deployServiceWorkers(image string) {
|
|||||||
nodeCount = uint64(cnt)
|
nodeCount = uint64(cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
appReplicas := os.Getenv("SHUFFLE_APP_REPLICAS")
|
appReplicas := os.Getenv("SHUFFLE_APP_REPLICAS")
|
||||||
appReplicaCnt := 2
|
appReplicaCnt := 2
|
||||||
if len(appReplicas) > 0 {
|
if len(appReplicas) > 0 {
|
||||||
@@ -799,6 +800,48 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar {
|
|||||||
return envVars
|
return envVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildResourcesFromEnv() corev1.ResourceRequirements {
|
||||||
|
requests := corev1.ResourceList{}
|
||||||
|
limits := corev1.ResourceList{}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
env string
|
||||||
|
resourceName corev1.ResourceName
|
||||||
|
resourceList corev1.ResourceList
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []item{
|
||||||
|
// kubernetes requests
|
||||||
|
{env: "SHUFFLE_WORKER_CPU_REQUEST", resourceName: corev1.ResourceCPU, resourceList: requests},
|
||||||
|
{env: "SHUFFLE_WORKER_MEMORY_REQUEST", resourceName: corev1.ResourceMemory, resourceList: requests},
|
||||||
|
{env: "SHUFFLE_WORKER_EPHEMERAL_STORAGE_REQUEST", resourceName: corev1.ResourceEphemeralStorage, resourceList: requests},
|
||||||
|
// kubernetes limits
|
||||||
|
{env: "SHUFFLE_WORKER_CPU_LIMIT", resourceName: corev1.ResourceCPU, resourceList: limits},
|
||||||
|
{env: "SHUFFLE_WORKER_MEMORY_LIMIT", resourceName: corev1.ResourceMemory, resourceList: limits},
|
||||||
|
{env: "SHUFFLE_WORKER_EPHEMERAL_STORAGE_LIMIT", resourceName: corev1.ResourceEphemeralStorage, resourceList: limits},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
if value := strings.TrimSpace(os.Getenv(it.env)); value != "" {
|
||||||
|
if quantity, err := resource.ParseQuantity(value); err == nil {
|
||||||
|
it.resourceList[it.resourceName] = quantity
|
||||||
|
} else {
|
||||||
|
log.Printf("[WARNING] Cannot parse %s=%q as resource quantity: %v", it.env, value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := corev1.ResourceRequirements{}
|
||||||
|
if len(requests) > 0 {
|
||||||
|
rr.Requests = requests
|
||||||
|
}
|
||||||
|
if len(limits) > 0 {
|
||||||
|
rr.Limits = limits
|
||||||
|
}
|
||||||
|
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
|
||||||
func handleBackendImageDownload(ctx context.Context, images string) error {
|
func handleBackendImageDownload(ctx context.Context, images string) error {
|
||||||
|
|
||||||
// Replicate images with lowercase, as the name may be wrong
|
// Replicate images with lowercase, as the name may be wrong
|
||||||
@@ -1042,6 +1085,20 @@ func deployK8sWorker(image string, identifier string, env []string) error {
|
|||||||
env = append(env, fmt.Sprintf("IS_KUBERNETES=true"))
|
env = append(env, fmt.Sprintf("IS_KUBERNETES=true"))
|
||||||
env = append(env, fmt.Sprintf("KUBERNETES_NAMESPACE=%s", os.Getenv("KUBERNETES_NAMESPACE")))
|
env = append(env, fmt.Sprintf("KUBERNETES_NAMESPACE=%s", os.Getenv("KUBERNETES_NAMESPACE")))
|
||||||
|
|
||||||
|
// app resource env
|
||||||
|
for _, k := range []string{
|
||||||
|
"SHUFFLE_APP_CPU_REQUEST",
|
||||||
|
"SHUFFLE_APP_MEMORY_REQUEST",
|
||||||
|
"SHUFFLE_APP_EPHEMERAL_STORAGE_REQUEST",
|
||||||
|
"SHUFFLE_APP_CPU_LIMIT",
|
||||||
|
"SHUFFLE_APP_MEMORY_LIMIT",
|
||||||
|
"SHUFFLE_APP_EPHEMERAL_STORAGE_LIMIT",
|
||||||
|
} {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(os.Getenv("KUBERNETES_SERVICE_HOST")) > 0 {
|
if len(os.Getenv("KUBERNETES_SERVICE_HOST")) > 0 {
|
||||||
env = append(env, fmt.Sprintf("KUBERNETES_SERVICE_HOST=%s", os.Getenv("KUBERNETES_SERVICE_HOST")))
|
env = append(env, fmt.Sprintf("KUBERNETES_SERVICE_HOST=%s", os.Getenv("KUBERNETES_SERVICE_HOST")))
|
||||||
}
|
}
|
||||||
@@ -1089,10 +1146,18 @@ func deployK8sWorker(image string, identifier string, env []string) error {
|
|||||||
env = append(env, fmt.Sprintf("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT=%s", appContainerSecurityContext))
|
env = append(env, fmt.Sprintf("SHUFFLE_APP_CONTAINER_SECURITY_CONTEXT=%s", appContainerSecurityContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(os.Getenv("SHUFFLE_APP_MOUNT_TMP_VOLUME")) > 0 {
|
||||||
|
env = append(env, fmt.Sprintf("SHUFFLE_APP_MOUNT_TMP_VOLUME=%s", os.Getenv("SHUFFLE_APP_MOUNT_TMP_VOLUME")))
|
||||||
|
}
|
||||||
|
|
||||||
if len(os.Getenv("SHUFFLE_LOGS_DISABLED")) > 0 {
|
if len(os.Getenv("SHUFFLE_LOGS_DISABLED")) > 0 {
|
||||||
env = append(env, fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", os.Getenv("SHUFFLE_LOGS_DISABLED")))
|
env = append(env, fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", os.Getenv("SHUFFLE_LOGS_DISABLED")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(os.Getenv("SHUFFLE_APP_REPLICAS")) > 0 {
|
||||||
|
env = append(env, fmt.Sprintf("SHUFFLE_APP_REPLICAS=%s", os.Getenv("SHUFFLE_APP_REPLICAS")))
|
||||||
|
}
|
||||||
|
|
||||||
clientset, _, err := shuffle.GetKubernetesClient()
|
clientset, _, err := shuffle.GetKubernetesClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Error getting kubernetes client:", err)
|
log.Printf("[ERROR] Error getting kubernetes client:", err)
|
||||||
@@ -1206,6 +1271,7 @@ func deployK8sWorker(image string, identifier string, env []string) error {
|
|||||||
Image: kubernetesImage,
|
Image: kubernetesImage,
|
||||||
Env: buildEnvVars(envMap),
|
Env: buildEnvVars(envMap),
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: buildResourcesFromEnv(),
|
||||||
|
|
||||||
//ImagePullPolicy: "Never",
|
//ImagePullPolicy: "Never",
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
@@ -1337,6 +1403,7 @@ func deployK8sWorker(image string, identifier string, env []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svcAppProtocol := "http"
|
||||||
service := &corev1.Service{
|
service := &corev1.Service{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: identifier,
|
Name: identifier,
|
||||||
@@ -1346,9 +1413,10 @@ func deployK8sWorker(image string, identifier string, env []string) error {
|
|||||||
Selector: matchLabels,
|
Selector: matchLabels,
|
||||||
Ports: []corev1.ServicePort{
|
Ports: []corev1.ServicePort{
|
||||||
{
|
{
|
||||||
Protocol: "TCP",
|
Protocol: "TCP",
|
||||||
Port: 33333,
|
AppProtocol: &svcAppProtocol,
|
||||||
TargetPort: intstr.FromInt(33333),
|
Port: 33333,
|
||||||
|
TargetPort: intstr.FromInt(33333),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Type: corev1.ServiceTypeClusterIP,
|
Type: corev1.ServiceTypeClusterIP,
|
||||||
@@ -1571,12 +1639,12 @@ func initializeImages() {
|
|||||||
|
|
||||||
if appSdkVersion == "" {
|
if appSdkVersion == "" {
|
||||||
appSdkVersion = "latest"
|
appSdkVersion = "latest"
|
||||||
log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %#v", appSdkVersion)
|
log.Printf("[INFO] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %#v", appSdkVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if workerVersion == "" {
|
if workerVersion == "" {
|
||||||
workerVersion = "latest"
|
workerVersion = "latest"
|
||||||
log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %#v", workerVersion)
|
log.Printf("[INFO] SHUFFLE_WORKER_VERSION not defined. Defaulting to %#v", workerVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseimageregistry == "" {
|
if baseimageregistry == "" {
|
||||||
@@ -1586,12 +1654,12 @@ func initializeImages() {
|
|||||||
if len(os.Getenv("REGISTRY_URL")) > 0 {
|
if len(os.Getenv("REGISTRY_URL")) > 0 {
|
||||||
baseimageregistry = os.Getenv("REGISTRY_URL")
|
baseimageregistry = os.Getenv("REGISTRY_URL")
|
||||||
} else {
|
} else {
|
||||||
os.Setenv("REGISTRY_URL", baseimageregistry)
|
// os.Setenv("REGISTRY_URL", baseimageregistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("SHUFFLE_BASE_IMAGE_REGISTRY", baseimageregistry)
|
os.Setenv("SHUFFLE_BASE_IMAGE_REGISTRY", baseimageregistry)
|
||||||
|
|
||||||
log.Printf("[WARNING] Setting baseimageregistry to %#v", baseimageregistry)
|
log.Printf("[INFO] Setting baseimageregistry to %#v", baseimageregistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseimagename == "" {
|
if baseimagename == "" {
|
||||||
@@ -1600,7 +1668,7 @@ func initializeImages() {
|
|||||||
baseimagename = "frikky/shuffle" // Dockerhub
|
baseimagename = "frikky/shuffle" // Dockerhub
|
||||||
|
|
||||||
os.Setenv("SHUFFLE_BASE_IMAGE_NAME", baseimagename)
|
os.Setenv("SHUFFLE_BASE_IMAGE_NAME", baseimagename)
|
||||||
log.Printf("[WARNING] Setting baseimagename to %#v", baseimagename)
|
log.Printf("[INFO] Setting baseimagename to %#v", baseimagename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Old sane default overrides:
|
// Old sane default overrides:
|
||||||
@@ -2063,12 +2131,29 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[INFO] Setting up execution environment")
|
// Auto enables pipelines IF they are not mentioned
|
||||||
|
if len(os.Getenv("SHUFFLE_SKIP_PIPELINES")) == 0 {
|
||||||
|
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
||||||
|
os.Setenv("SHUFFLE_PIPELINE_ENABLED", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SHUFFLE_SKIP_PIPELINES") != "true" && os.Getenv("SHUFFLE_PIPELINE_ENABLED") != "false" {
|
||||||
|
// Run in 15 seconds in a goroutine
|
||||||
|
go func() {
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
log.Printf("[INFO] Auto-downloading Sigma rules during startup")
|
||||||
|
ruleType := "sigma"
|
||||||
|
err := handleFileCategoryChange(ruleType)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARNING] Failed downloading %s rules: %s", ruleType, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[INFO] Setting up execution environment for env '%s'", environment)
|
||||||
// //FIXME
|
// //FIXME
|
||||||
if baseUrl == "" {
|
if baseUrl == "" {
|
||||||
baseUrl = "https://shuffler.io"
|
baseUrl = "https://shuffler.io"
|
||||||
//baseUrl = "http://localhost:5001"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(orborusUuid) == 0 {
|
if len(orborusUuid) == 0 {
|
||||||
@@ -2229,7 +2314,6 @@ func main() {
|
|||||||
fullUrl += "?amount=50"
|
fullUrl += "?amount=50"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if isKubernetes == "true" {
|
if isKubernetes == "true" {
|
||||||
log.Printf("[INFO] Finished configuring kubernetes environment. Connecting to %s", fullUrl)
|
log.Printf("[INFO] Finished configuring kubernetes environment. Connecting to %s", fullUrl)
|
||||||
} else {
|
} else {
|
||||||
@@ -2432,9 +2516,10 @@ func main() {
|
|||||||
for _, incRequest := range executionRequests.Data {
|
for _, incRequest := range executionRequests.Data {
|
||||||
|
|
||||||
// Looking for specific jobs
|
// Looking for specific jobs
|
||||||
if incRequest.Type == "PIPELINE_CREATE" || incRequest.Type == "PIPELINE_START" || incRequest.Type == "PIPELINE_STOP" || incRequest.Type == "PIPELINE_DELETE" {
|
if incRequest.Type == "PIPELINE_CREATE" || incRequest.Type == "PIPELINE_START" || incRequest.Type == "PIPELINE_STOP" || incRequest.Type == "PIPELINE_DELETE" || incRequest.Type == "PIPELINE_UPDATE" {
|
||||||
|
log.Printf("[INFO] Handling pipeline request from backend: '%s' with argument '%s'", incRequest.Type, incRequest.ExecutionArgument)
|
||||||
|
|
||||||
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
//os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
||||||
tenzirDisabled = false
|
tenzirDisabled = false
|
||||||
|
|
||||||
// Running NEW or editing pipelines
|
// Running NEW or editing pipelines
|
||||||
@@ -2457,9 +2542,9 @@ func main() {
|
|||||||
|
|
||||||
} else if incRequest.Type == "CATEGORY_UPDATE" {
|
} else if incRequest.Type == "CATEGORY_UPDATE" {
|
||||||
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
||||||
tenzirDisabled = false
|
|
||||||
|
|
||||||
err = handleFileCategoryChange()
|
tenzirDisabled = false
|
||||||
|
err = handleFileCategoryChange("sigma")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to download the file category: %s", err)
|
log.Printf("[ERROR] Failed to download the file category: %s", err)
|
||||||
}
|
}
|
||||||
@@ -2469,7 +2554,7 @@ func main() {
|
|||||||
} else if incRequest.Type == "DISABLE_SIGMA_FOLDER" {
|
} else if incRequest.Type == "DISABLE_SIGMA_FOLDER" {
|
||||||
log.Printf("[INFO] Got job to disable sigma rules")
|
log.Printf("[INFO] Got job to disable sigma rules")
|
||||||
|
|
||||||
err = removeFileCategory()
|
err = removeFileCategory("sigma")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to disable the sigma rules: %s", err)
|
log.Printf("[ERROR] Failed to disable the sigma rules: %s", err)
|
||||||
}
|
}
|
||||||
@@ -2501,7 +2586,7 @@ func main() {
|
|||||||
log.Printf("[INFO] Got job to start tenzir")
|
log.Printf("[INFO] Got job to start tenzir")
|
||||||
|
|
||||||
// Manual command = overrides to allow starting of Tenzir from the frontend anyway.
|
// Manual command = overrides to allow starting of Tenzir from the frontend anyway.
|
||||||
os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
//os.Setenv("SHUFFLE_SKIP_PIPELINES", "false")
|
||||||
tenzirDisabled = false
|
tenzirDisabled = false
|
||||||
|
|
||||||
// Removed either way
|
// Removed either way
|
||||||
@@ -2511,8 +2596,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(fmt.Sprintf("%s", err), "node available") {
|
if strings.Contains(fmt.Sprintf("%s", err), "node available") {
|
||||||
// Disabling until UI is updated
|
// Disabling until UI is updated
|
||||||
os.Setenv("SHUFFLE_SKIP_PIPELINES", "true")
|
//os.Setenv("SHUFFLE_SKIP_PIPELINES", "true")
|
||||||
tenzirDisabled = true
|
//tenzirDisabled = true
|
||||||
|
|
||||||
log.Printf("[ERROR] Failed to start tenzir, reason: %s", err)
|
log.Printf("[ERROR] Failed to start tenzir, reason: %s", err)
|
||||||
err = shuffle.CreateOrgNotification(
|
err = shuffle.CreateOrgNotification(
|
||||||
@@ -2578,6 +2663,8 @@ func main() {
|
|||||||
executionRequests.Data = executionRequests.Data[0:allowed]
|
executionRequests.Data = executionRequests.Data[0:allowed]
|
||||||
}
|
}
|
||||||
} else if swarmControlMode && (swarmConfig == "run" || swarmConfig == "swarm") {
|
} else if swarmControlMode && (swarmConfig == "run" || swarmConfig == "swarm") {
|
||||||
|
// any reason it is not maxConcurrency instead of
|
||||||
|
// hardcoded 50?
|
||||||
if len(executionRequests.Data) > 50 {
|
if len(executionRequests.Data) > 50 {
|
||||||
executionRequests.Data = executionRequests.Data[0:50]
|
executionRequests.Data = executionRequests.Data[0:50]
|
||||||
}
|
}
|
||||||
@@ -2758,7 +2845,7 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error {
|
|||||||
|
|
||||||
// no need of execution arguments for STOP and DELETE
|
// no need of execution arguments for STOP and DELETE
|
||||||
if (incRequest.Type != "PIPELINE_STOP" && incRequest.Type != "PIPELINE_DELETE") && len(incRequest.ExecutionArgument) == 0 {
|
if (incRequest.Type != "PIPELINE_STOP" && incRequest.Type != "PIPELINE_DELETE") && len(incRequest.ExecutionArgument) == 0 {
|
||||||
log.Printf("[ERROR] No execution argument found for pipeline create. Skipping")
|
log.Printf("[ERROR] No execution argument found for pipeline type %s. Skipping", incRequest.Type)
|
||||||
|
|
||||||
return errors.New("no execution argument found for pipeline create. Skipping")
|
return errors.New("no execution argument found for pipeline create. Skipping")
|
||||||
}
|
}
|
||||||
@@ -2769,6 +2856,7 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command := incRequest.ExecutionArgument
|
command := incRequest.ExecutionArgument
|
||||||
|
pipelines = []shuffle.PipelineInfo{}
|
||||||
if incRequest.Type == "PIPELINE_CREATE" {
|
if incRequest.Type == "PIPELINE_CREATE" {
|
||||||
log.Printf("[INFO] Should delete -> recreate new pipeline with id %#v", identifier)
|
log.Printf("[INFO] Should delete -> recreate new pipeline with id %#v", identifier)
|
||||||
//err := deployPipeline(image, identifier, command)
|
//err := deployPipeline(image, identifier, command)
|
||||||
@@ -2778,19 +2866,16 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if incRequest.Type == "PIPELINE_DELETE" || incRequest.Type == "PIPELINE_STOP" {
|
} else if incRequest.Type == "PIPELINE_DELETE" || incRequest.Type == "PIPELINE_STOP" {
|
||||||
{
|
pipelineId := incRequest.ExecutionId
|
||||||
log.Printf("[INFO] Should delete pipeline %#v", identifier)
|
log.Printf("[INFO] Should delete pipeline %#v. PipelineID: %s", identifier, pipelineId)
|
||||||
pipelineId, err := searchPipeline(identifier)
|
//pipelineId, err := searchPipeline(identifier)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
log.Printf("[ERROR] Failed searching for Pipeline with name %s reason:%s ", identifier, err)
|
//}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = deletePipeline(pipelineId)
|
err = deletePipeline(pipelineId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed Deleting Pipeline %s", err)
|
log.Printf("[ERROR] Failed Deleting Pipeline %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2817,19 +2902,24 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "no existing pipeline found with name" {
|
if err.Error() == "no existing pipeline found with name" {
|
||||||
log.Printf("[INFO] Starting a new pipeline with command '%s' and identifier '%s'", command, identifier)
|
log.Printf("[INFO] Starting a new pipeline with command '%s' and identifier '%s'", command, identifier)
|
||||||
_, CreateErr := createPipeline(command, identifier)
|
var createErr error
|
||||||
return CreateErr
|
pipelineId, createErr = createPipeline(command, identifier)
|
||||||
|
if createErr != nil {
|
||||||
|
return createErr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[ERROR] Failed searching for Pipeline with name %s reason:%s ", identifier, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[ERROR] Failed searching for Pipeline with name %s reason:%s ", identifier, err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Starting existing pipeline with ID %s", pipelineId)
|
||||||
_, err = updatePipelineState(command, pipelineId, "start")
|
_, err = updatePipelineState(command, pipelineId, "start")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to start Pipeline: %s reason:%s ", pipelineId, err)
|
log.Printf("[ERROR] Failed to start Pipeline: %s reason:%s ", pipelineId, err)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[INFO] Successfully started the Pipeline: %s", pipelineId)
|
log.Printf("[INFO] Successfully started pipeline: %s", pipelineId)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -2859,27 +2949,38 @@ func deployTenzirNode() error {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
cacheKey := "tenzir-key"
|
cacheKey := "tenzir-key"
|
||||||
|
|
||||||
imageName := "frikky/shuffle:tenzir"
|
|
||||||
containerName := "tenzir-node"
|
|
||||||
containerStartOptions := container.StartOptions{}
|
|
||||||
_, err = shuffle.GetCache(ctx, cacheKey)
|
_, err = shuffle.GetCache(ctx, cacheKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageName := "frikky/shuffle:tenzir"
|
||||||
|
if os.Getenv("TENZIR_IMAGE_NAME") != "" {
|
||||||
|
imageName = os.Getenv("TENZIR_IMAGE_NAME")
|
||||||
|
log.Printf("[INFO] Using custom Tenzir image name: %s", imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerName := "tenzir-node"
|
||||||
|
containerStartOptions := container.StartOptions{}
|
||||||
|
|
||||||
containerInfo, err := dockercli.ContainerInspect(ctx, containerName)
|
containerInfo, err := dockercli.ContainerInspect(ctx, containerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if dockerclient.IsErrNotFound(err) {
|
if dockerclient.IsErrNotFound(err) {
|
||||||
// Create network if it doesn't exist
|
// Create network if it doesn't exist
|
||||||
networkName := "tenzir-network"
|
networkName := "tenzir-network"
|
||||||
networkSubnet := "192.168.1.0/24"
|
networkSubnet := "192.168.102.0/24"
|
||||||
networkGateway := "192.168.1.1"
|
networkGateway := "192.168.102.1"
|
||||||
|
|
||||||
err = createNetworkIfNotExists(ctx, networkName, networkSubnet, networkGateway)
|
err = createNetworkIfNotExists(ctx, networkName, networkSubnet, networkGateway)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to create network: %s", err)
|
log.Printf("[ERROR] Failed to create network %s: %s", networkName, err)
|
||||||
return err
|
//return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trying to connect orborus to the tenzir network as well
|
||||||
|
err = dockercli.NetworkConnect(ctx, networkName, containerId, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Error connecting tenzir container to network: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if image exists
|
// Check if image exists
|
||||||
@@ -2908,7 +3009,7 @@ func deployTenzirNode() error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !containerInfo.State.Running {
|
if !containerInfo.State.Running {
|
||||||
log.Printf("[DEBUG] Tenzir Node exists but is not running. Restarting it.")
|
log.Printf("[DEBUG] Tenzir Node exists, but is not running. Restarting it.")
|
||||||
err := dockercli.ContainerStart(ctx, containerName, containerStartOptions)
|
err := dockercli.ContainerStart(ctx, containerName, containerStartOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to start Tenzir Node container: %v", err)
|
log.Printf("[ERROR] Failed to start Tenzir Node container: %v", err)
|
||||||
@@ -2934,6 +3035,7 @@ func deployTenzirNode() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARNING] Failed marshalling execution: %s", err)
|
log.Printf("[WARNING] Failed marshalling execution: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = shuffle.SetCache(ctx, cacheKey, cacheData, 1)
|
err = shuffle.SetCache(ctx, cacheKey, cacheData, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARNING] Failed updating cache for tenzir: %s", err)
|
log.Printf("[WARNING] Failed updating cache for tenzir: %s", err)
|
||||||
@@ -2957,8 +3059,8 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
|||||||
Healthcheck: healthconfig,
|
Healthcheck: healthconfig,
|
||||||
ExposedPorts: nat.PortSet{
|
ExposedPorts: nat.PortSet{
|
||||||
"5160/tcp": struct{}{},
|
"5160/tcp": struct{}{},
|
||||||
"514/udp": struct{}{},
|
"1514/udp": struct{}{},
|
||||||
"514/tcp": struct{}{},
|
"1514/tcp": struct{}{},
|
||||||
},
|
},
|
||||||
Entrypoint: []string{containerName},
|
Entrypoint: []string{containerName},
|
||||||
Env: []string{},
|
Env: []string{},
|
||||||
@@ -3004,26 +3106,28 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
|||||||
|
|
||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
PortBindings: nat.PortMap{
|
PortBindings: nat.PortMap{
|
||||||
"514/tcp": []nat.PortBinding{{HostPort: "514"}},
|
"1514/tcp": []nat.PortBinding{{HostPort: "1514"}},
|
||||||
"514/udp": []nat.PortBinding{{HostPort: "514"}},
|
"1514/udp": []nat.PortBinding{{HostPort: "1514"}},
|
||||||
"5160/tcp": []nat.PortBinding{{HostPort: "5160"}},
|
"5160/tcp": []nat.PortBinding{{HostPort: "5160"}},
|
||||||
},
|
},
|
||||||
Mounts: []mount.Mount{
|
Mounts: []mount.Mount{
|
||||||
{
|
{
|
||||||
Type: "bind",
|
Type: "bind",
|
||||||
Source: tenzirStorageFolder,
|
Source: tenzirStorageFolder,
|
||||||
Target: "/var/lib/tenzir/",
|
Target: "/tmp",
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bind",
|
|
||||||
Source: tenzirStorageFolder,
|
|
||||||
Target: "/var/log/tenzir/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "bind",
|
|
||||||
Source: tenzirStorageFolder,
|
|
||||||
Target: "/var/cache/tenzir/",
|
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
Type: "bind",
|
||||||
|
Source: tenzirStorageFolder,
|
||||||
|
Target: "/var/log/tenzir/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "bind",
|
||||||
|
Source: tenzirStorageFolder,
|
||||||
|
Target: "/var/cache/tenzir/",
|
||||||
|
},
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
VolumeDriver: "local",
|
VolumeDriver: "local",
|
||||||
RestartPolicy: container.RestartPolicy{
|
RestartPolicy: container.RestartPolicy{
|
||||||
@@ -3031,6 +3135,12 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SHUFFLE_DISABLE_SYSLOG") == "true" {
|
||||||
|
hostConfig.PortBindings = nat.PortMap{
|
||||||
|
"5160/tcp": []nat.PortBinding{{HostPort: "5160"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if skipPipelineMount {
|
if skipPipelineMount {
|
||||||
hostConfig.Mounts = []mount.Mount{}
|
hostConfig.Mounts = []mount.Mount{}
|
||||||
}
|
}
|
||||||
@@ -3039,7 +3149,7 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
|||||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||||
"tenzir-network": {
|
"tenzir-network": {
|
||||||
IPAMConfig: &network.EndpointIPAMConfig{
|
IPAMConfig: &network.EndpointIPAMConfig{
|
||||||
IPv4Address: "192.168.1.100",
|
IPv4Address: "192.168.102.100",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -3093,12 +3203,19 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Successfully deployed Tenzir Node! Setting up default syslog listener on UDP 514")
|
log.Printf("[INFO] Successfully deployed Tenzir Node! Setting up default syslog listener on TCP/1514 AND UDP/1514")
|
||||||
|
|
||||||
command := "from udp://0.0.0.0:514 read syslog | import"
|
command := `from "tcp://0.0.0.0:1514" { read_syslog } | import`
|
||||||
_, err = createPipeline(command, "default-syslog-514")
|
_, err = createPipeline(command, "default-syslog-tcp-514")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to create default syslog pipeline: %s", err)
|
log.Printf("[ERROR] Failed to create tcp syslog pipeline: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
command = `load_udp "0.0.0.0:1514", insert_newlines=true | read_syslog | import`
|
||||||
|
_, err = createPipeline(command, "default-syslog-udp-514")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to create udp syslog pipeline: %s", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3143,7 +3260,7 @@ func createNetworkIfNotExists(ctx context.Context, networkName, subnet, gateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkTenzirNode() error {
|
func checkTenzirNode() error {
|
||||||
if os.Getenv("SHUFFLE_SKIP_PIPELINES") == "true" {
|
if os.Getenv("SHUFFLE_SKIP_PIPELINES") == "true" && os.Getenv("SHUFFLE_PIPELINE_ENABLED") == "false" {
|
||||||
return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
|
return errors.New("Pipelines are disabled by user with SHUFFLE_SKIP_PIPELINES")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3216,21 +3333,18 @@ func createPipeline(command, identifier string) (string, error) {
|
|||||||
//command = "from file /var/lib/tenzir/sysmon_logs.ndjson read json | sigma /var/lib/tenzir/rule.yaml"
|
//command = "from file /var/lib/tenzir/sysmon_logs.ndjson read json | sigma /var/lib/tenzir/rule.yaml"
|
||||||
//command = "from file /var/lib/tenzir/sysmon_logs.ndjson read json | import"
|
//command = "from file /var/lib/tenzir/sysmon_logs.ndjson read json | import"
|
||||||
|
|
||||||
|
// Make sure to escape them
|
||||||
|
//if strings.Contains(command, "/") {
|
||||||
|
// command = strings.ReplaceAll("\\\"", "", command)
|
||||||
|
// command = strings.ReplaceAll(command, "\"", "")
|
||||||
|
//}
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"definition": command,
|
"definition": command,
|
||||||
"name": identifier,
|
"name": identifier,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"autostart": map[string]bool{
|
|
||||||
"created": true,
|
|
||||||
"completed": false,
|
|
||||||
"failed": false,
|
|
||||||
},
|
|
||||||
"autodelete": map[string]bool{
|
|
||||||
"completed": false,
|
|
||||||
"failed": false,
|
|
||||||
"stopped": false,
|
|
||||||
},
|
|
||||||
"retry_delay": "500.0ms",
|
"retry_delay": "500.0ms",
|
||||||
|
"unstoppable": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBodyJSON, err := json.Marshal(requestBody)
|
requestBodyJSON, err := json.Marshal(requestBody)
|
||||||
@@ -3240,18 +3354,18 @@ func createPipeline(command, identifier string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
forwardData := bytes.NewBuffer(requestBodyJSON)
|
forwardData := bytes.NewBuffer(requestBodyJSON)
|
||||||
|
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequest(
|
||||||
forwardMethod,
|
forwardMethod,
|
||||||
url,
|
url,
|
||||||
forwardData,
|
forwardData,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to create HTTP request: %s", err)
|
log.Printf("[ERROR] Failed to create HTTP request: %s", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3266,7 +3380,9 @@ func createPipeline(command, identifier string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(string(body), "error") {
|
if strings.Contains(string(body), "error") {
|
||||||
log.Printf("[ERROR] Pipeline creation response (%d): %s", resp.StatusCode, string(body))
|
log.Printf("[ERROR] Pipeline creation error resp (%d): %s", resp.StatusCode, string(body))
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] Pipeline creation debug (%d): %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -3292,37 +3408,39 @@ func createPipeline(command, identifier string) (string, error) {
|
|||||||
return "", errors.New("Pipeline ID not found or empty in the response. See error logs.")
|
return "", errors.New("Pipeline ID not found or empty in the response. See error logs.")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := response.ID
|
return response.ID, nil
|
||||||
return id, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePipelineState(command, pipelineId, action string) (string, error) {
|
func updatePipelineState(command, pipelineId, action string) (string, error) {
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/v0/pipeline/update", pipelineUrl)
|
url := fmt.Sprintf("%s/api/v0/pipeline/update", pipelineUrl)
|
||||||
forwardMethod := "POST"
|
forwardMethod := "POST"
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"id": pipelineId,
|
"id": pipelineId,
|
||||||
"definition": command,
|
"action": action,
|
||||||
"action": action,
|
|
||||||
"autostart": map[string]bool{
|
/*
|
||||||
"created": true,
|
"autostart": map[string]bool{
|
||||||
"completed": true,
|
"created": true,
|
||||||
"failed": true,
|
"completed": false,
|
||||||
},
|
"failed": false,
|
||||||
"autodelete": map[string]bool{
|
},
|
||||||
"completed": false,
|
"autodelete": map[string]bool{
|
||||||
"failed": false,
|
"completed": false,
|
||||||
"stopped": false,
|
"failed": false,
|
||||||
},
|
"stopped": false,
|
||||||
|
},
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBodyJSON, err := json.Marshal(requestBody)
|
requestBodyJSON, err := json.Marshal(requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
forwardData := bytes.NewBuffer(requestBodyJSON)
|
|
||||||
|
|
||||||
|
log.Printf("[INFO] Updating pipeline %s with action %s to ensure it starts. Body: %s", pipelineId, action, string(requestBodyJSON))
|
||||||
|
|
||||||
|
forwardData := bytes.NewBuffer(requestBodyJSON)
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequest(
|
||||||
forwardMethod,
|
forwardMethod,
|
||||||
url,
|
url,
|
||||||
@@ -3405,7 +3523,7 @@ func deletePipeline(pipelineId string) error {
|
|||||||
|
|
||||||
log.Printf("[INFO] Pipeline with ID: %s deleted successfully", pipelineId)
|
log.Printf("[INFO] Pipeline with ID: %s deleted successfully", pipelineId)
|
||||||
|
|
||||||
pipelines = []shuffle.PipelineInfoMini{}
|
pipelines = []shuffle.PipelineInfo{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3471,15 +3589,22 @@ func searchPipeline(identifier string) (string, error) {
|
|||||||
return "", errors.New("no existing pipeline found with name")
|
return "", errors.New("no existing pipeline found with name")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFileCategoryChange() error {
|
func handleFileCategoryChange(ruleType string) error {
|
||||||
apiEndpoint := baseUrl + "/api/v1/files/namespaces/sigma"
|
apiEndpoint := fmt.Sprintf("%s/api/v1/files/namespaces/%s", baseUrl, ruleType)
|
||||||
req, err := http.NewRequest("GET", apiEndpoint, nil)
|
req, err := http.NewRequest("GET", apiEndpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pipelineApikey) == 0 {
|
if len(pipelineApikey) == 0 {
|
||||||
return errors.New("Shuffle API-key not set for Pipelines: SHUFFLE_PIPELINE_AUTH=<apikey>")
|
//var auth = os.Getenv("AUTH")
|
||||||
|
//var org = os.Getenv("ORG")
|
||||||
|
|
||||||
|
if len(auth) > 0 && len(org) > 0 {
|
||||||
|
pipelineApikey = auth
|
||||||
|
} else {
|
||||||
|
return errors.New("Shuffle API-key not set for Pipelines: SHUFFLE_PIPELINE_AUTH=<apikey>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Authorization", "Bearer "+pipelineApikey)
|
req.Header.Add("Authorization", "Bearer "+pipelineApikey)
|
||||||
@@ -3512,14 +3637,13 @@ func handleFileCategoryChange() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//log.Println("[DEBUG] ZIP file downloaded successfully.")
|
//log.Println("[DEBUG] ZIP file downloaded successfully.")
|
||||||
|
|
||||||
tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER")
|
tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER")
|
||||||
if len(tenzirStorageFolder) == 0 {
|
if len(tenzirStorageFolder) == 0 {
|
||||||
tenzirStorageFolder = "/tmp/"
|
tenzirStorageFolder = "/tmp/"
|
||||||
}
|
}
|
||||||
|
|
||||||
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
|
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
|
||||||
sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder)
|
sigmaPath := fmt.Sprintf("%s/%s_rules", tenzirStorageFolder, ruleType)
|
||||||
err = extractZIP("files.zip", sigmaPath)
|
err = extractZIP("files.zip", sigmaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to extract ZIP file: %s", err)
|
log.Printf("[ERROR] Failed to extract ZIP file: %s", err)
|
||||||
@@ -3544,12 +3668,12 @@ func extractZIP(zipFile, destDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Total size of the ZIP file: %d bytes", totalSize)
|
log.Printf("[DEBUG] Total size of the ZIP file: %d bytes", totalSize)
|
||||||
|
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Total files to extract: %d", len(r.File))
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
// Fix path traversal
|
// Fix path traversal
|
||||||
if strings.Contains(f.Name, "..") {
|
if strings.Contains(f.Name, "..") {
|
||||||
@@ -3570,16 +3694,15 @@ func extractFile(f *zip.File, destDir string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rc.Close()
|
defer rc.Close()
|
||||||
|
|
||||||
path := filepath.Join(destDir, f.Name)
|
path := filepath.Join(destDir, f.Name)
|
||||||
|
|
||||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
|
defer out.Close()
|
||||||
_, err = io.Copy(out, rc)
|
_, err = io.Copy(out, rc)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -3608,7 +3731,7 @@ func copyToTenzir(srcPath, destPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFileCategory() error {
|
func removeFileCategory(ruleType string) error {
|
||||||
tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER")
|
tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER")
|
||||||
if len(tenzirStorageFolder) == 0 {
|
if len(tenzirStorageFolder) == 0 {
|
||||||
tenzirStorageFolder = "/tmp/"
|
tenzirStorageFolder = "/tmp/"
|
||||||
@@ -3617,18 +3740,19 @@ func removeFileCategory() error {
|
|||||||
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
|
tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/")
|
||||||
|
|
||||||
//sigmaPath := "/var/lib/tenzir/sigma_rules/*"
|
//sigmaPath := "/var/lib/tenzir/sigma_rules/*"
|
||||||
sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder)
|
rulePath := fmt.Sprintf("%s/%s_rules", tenzirStorageFolder, ruleType)
|
||||||
|
|
||||||
err := os.RemoveAll(sigmaPath)
|
err := os.RemoveAll(rulePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error removing category files in %s: %v", sigmaPath, err)
|
return fmt.Errorf("Error removing category files in %s: %v", rulePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Removed all local category data in %s", sigmaPath)
|
log.Printf("[INFO] Removed all local category data in %s", rulePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// curl https://get.tenzir.app | sh
|
||||||
func removeFile(fileName string) error {
|
func removeFile(fileName string) error {
|
||||||
containerName := "tenzir-node"
|
containerName := "tenzir-node"
|
||||||
srcPath := fmt.Sprintf("/var/lib/tenzir/sigma_rules/%s", fileName)
|
srcPath := fmt.Sprintf("/var/lib/tenzir/sigma_rules/%s", fileName)
|
||||||
@@ -3658,7 +3782,7 @@ func removePath(containerName, path string) error {
|
|||||||
func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
|
func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
|
||||||
pipelinePayload := shuffle.LakeConfig{
|
pipelinePayload := shuffle.LakeConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Pipelines: []shuffle.PipelineInfoMini{},
|
Pipelines: []shuffle.PipelineInfo{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if tenzirDisabled {
|
if tenzirDisabled {
|
||||||
@@ -3670,18 +3794,9 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
|
|||||||
if len(pipelines) == 0 || randint == 0 {
|
if len(pipelines) == 0 || randint == 0 {
|
||||||
pipelineDef, err := listPipelines()
|
pipelineDef, err := listPipelines()
|
||||||
|
|
||||||
if err == nil {
|
if err == nil || len(pipelines) > 0 {
|
||||||
for _, pipeline := range pipelineDef {
|
pipelines = pipelineDef
|
||||||
pipelinePayload.Pipelines = append(pipelinePayload.Pipelines, shuffle.PipelineInfoMini{
|
pipelinePayload.Pipelines = pipelines
|
||||||
ID: pipeline.ID,
|
|
||||||
Name: pipeline.Name,
|
|
||||||
Definition: pipeline.Definition,
|
|
||||||
TotalRuns: pipeline.TotalRuns,
|
|
||||||
CreatedAt: pipeline.CreatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pipelines = pipelinePayload.Pipelines
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pipelinePayload.Pipelines = pipelines
|
pipelinePayload.Pipelines = pipelines
|
||||||
@@ -3694,7 +3809,7 @@ func sendPipelineHealthStatus() (shuffle.LakeConfig, error) {
|
|||||||
log.Printf("[ERROR] Tenzir node connection problem: %s", err)
|
log.Printf("[ERROR] Tenzir node connection problem: %s", err)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
tenzirDisabled = true
|
//tenzirDisabled = true
|
||||||
log.Printf("[WARNING] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err)
|
log.Printf("[WARNING] Disabling pipelines: %s. You will need to restart the Orborus to fix this.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3995,7 +4110,7 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
|
|||||||
|
|
||||||
// Specific to debugging
|
// Specific to debugging
|
||||||
if len(workerServerUrl) == 0 {
|
if len(workerServerUrl) == 0 {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[INFO] Using default worker server url as previous is invalid: %s. Swapping to shuffle-workers:33333", streamUrl)
|
log.Printf("[INFO] Using default worker server url as previous is invalid: %s. Swapping to shuffle-workers:33333", streamUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4107,6 +4222,10 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest, image string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0x0elliot:
|
||||||
|
// let's never increase worker replicas.
|
||||||
|
// in our tests, workers replicas mattered a lot less.
|
||||||
|
// edge-case: subflows are helped with when worker replicas are higher.
|
||||||
func AutoScale(ctx context.Context) {
|
func AutoScale(ctx context.Context) {
|
||||||
if os.Getenv("SHUFFLE_SCALE_REPLICAS") != "" {
|
if os.Getenv("SHUFFLE_SCALE_REPLICAS") != "" {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/docker/docker v28.3.3+incompatible
|
github.com/docker/docker v28.3.3+incompatible
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
github.com/shuffle/shuffle-shared v0.9.15
|
github.com/shuffle/shuffle-shared v0.9.30
|
||||||
github.com/shuffle/singul v0.0.16
|
github.com/shuffle/singul v0.0.16
|
||||||
k8s.io/api v0.33.1
|
k8s.io/api v0.33.1
|
||||||
k8s.io/apimachinery v0.33.1
|
k8s.io/apimachinery v0.33.1
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shuffle/shuffle-shared"
|
"github.com/shuffle/shuffle-shared"
|
||||||
"github.com/shuffle/singul/pkg"
|
singul "github.com/shuffle/singul/pkg"
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -43,6 +43,7 @@ import (
|
|||||||
//k8s deps
|
//k8s deps
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@@ -610,9 +611,9 @@ func deployk8sApp(image string, identifier string, env []string) error {
|
|||||||
|
|
||||||
// use deployment instead of pod
|
// use deployment instead of pod
|
||||||
// then expose a service similarly.
|
// then expose a service similarly.
|
||||||
// number of replicas can be set to os.Getenv("SHUFFLE_SCALE_REPLICAS")
|
// number of replicas can be set to os.Getenv("SHUFFLE_APP_REPLICAS")
|
||||||
replicaNumber := 1
|
replicaNumber := 1
|
||||||
replicaNumberStr := os.Getenv("SHUFFLE_SCALE_REPLICAS")
|
replicaNumberStr := os.Getenv("SHUFFLE_APP_REPLICAS")
|
||||||
if len(replicaNumberStr) > 0 {
|
if len(replicaNumberStr) > 0 {
|
||||||
tmpInt, err := strconv.Atoi(replicaNumberStr)
|
tmpInt, err := strconv.Atoi(replicaNumberStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -652,6 +653,7 @@ func deployk8sApp(image string, identifier string, env []string) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: containerSecurityContext,
|
||||||
|
Resources: buildResourcesFromEnv(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DNSPolicy: corev1.DNSClusterFirst,
|
DNSPolicy: corev1.DNSClusterFirst,
|
||||||
@@ -662,6 +664,27 @@ func deployk8sApp(image string, identifier string, env []string) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SHUFFLE_APP_MOUNT_TMP_VOLUME") == "true" {
|
||||||
|
deployment.Spec.Template.Spec.Volumes = append(
|
||||||
|
deployment.Spec.Template.Spec.Volumes,
|
||||||
|
corev1.Volume{
|
||||||
|
Name: "tmp",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
deployment.Spec.Template.Spec.Containers[0].VolumeMounts = append(
|
||||||
|
deployment.Spec.Template.Spec.Containers[0].VolumeMounts,
|
||||||
|
corev1.VolumeMount{
|
||||||
|
Name: "tmp",
|
||||||
|
ReadOnly: false,
|
||||||
|
MountPath: "/tmp",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if len(os.Getenv("REGISTRY_URL")) > 0 && len(os.Getenv("SHUFFLE_BASE_IMAGE_NAME")) > 0 {
|
if len(os.Getenv("REGISTRY_URL")) > 0 && len(os.Getenv("SHUFFLE_BASE_IMAGE_NAME")) > 0 {
|
||||||
log.Printf("[INFO] Setting image pull policy to Always as private registry is used.")
|
log.Printf("[INFO] Setting image pull policy to Always as private registry is used.")
|
||||||
//containerAttachment.ImagePullPolicy = corev1.PullAlways
|
//containerAttachment.ImagePullPolicy = corev1.PullAlways
|
||||||
@@ -676,6 +699,7 @@ func deployk8sApp(image string, identifier string, env []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svcAppProtocol := "http"
|
||||||
service := &corev1.Service{
|
service := &corev1.Service{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -685,9 +709,10 @@ func deployk8sApp(image string, identifier string, env []string) error {
|
|||||||
Selector: matchLabels,
|
Selector: matchLabels,
|
||||||
Ports: []corev1.ServicePort{
|
Ports: []corev1.ServicePort{
|
||||||
{
|
{
|
||||||
Protocol: "TCP",
|
Protocol: "TCP",
|
||||||
Port: 80,
|
AppProtocol: &svcAppProtocol,
|
||||||
TargetPort: intstr.FromInt(deployport),
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromInt(deployport),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Type: corev1.ServiceTypeClusterIP,
|
Type: corev1.ServiceTypeClusterIP,
|
||||||
@@ -2436,6 +2461,49 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar {
|
|||||||
}
|
}
|
||||||
return envVars
|
return envVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildResourcesFromEnv() corev1.ResourceRequirements {
|
||||||
|
requests := corev1.ResourceList{}
|
||||||
|
limits := corev1.ResourceList{}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
env string
|
||||||
|
resourceName corev1.ResourceName
|
||||||
|
resourceList corev1.ResourceList
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []item{
|
||||||
|
// kubernetes requests
|
||||||
|
{env: "SHUFFLE_APP_CPU_REQUEST", resourceName: corev1.ResourceCPU, resourceList: requests},
|
||||||
|
{env: "SHUFFLE_APP_MEMORY_REQUEST", resourceName: corev1.ResourceMemory, resourceList: requests},
|
||||||
|
{env: "SHUFFLE_APP_EPHEMERAL_STORAGE_REQUEST", resourceName: corev1.ResourceEphemeralStorage, resourceList: requests},
|
||||||
|
// kubernetes limits
|
||||||
|
{env: "SHUFFLE_APP_CPU_LIMIT", resourceName: corev1.ResourceCPU, resourceList: limits},
|
||||||
|
{env: "SHUFFLE_APP_MEMORY_LIMIT", resourceName: corev1.ResourceMemory, resourceList: limits},
|
||||||
|
{env: "SHUFFLE_APP_EPHEMERAL_STORAGE_LIMIT", resourceName: corev1.ResourceEphemeralStorage, resourceList: limits},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
if value := strings.TrimSpace(os.Getenv(it.env)); value != "" {
|
||||||
|
if quantity, err := resource.ParseQuantity(value); err == nil {
|
||||||
|
it.resourceList[it.resourceName] = quantity
|
||||||
|
} else {
|
||||||
|
log.Printf("[WARNING] Cannot parse %s=%q as resource quantity: %v", it.env, value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := corev1.ResourceRequirements{}
|
||||||
|
if len(requests) > 0 {
|
||||||
|
rr.Requests = requests
|
||||||
|
}
|
||||||
|
if len(limits) > 0 {
|
||||||
|
rr.Limits = limits
|
||||||
|
}
|
||||||
|
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
|
||||||
func getWorkerBackendExecution(auth string, executionId string) (*shuffle.WorkflowExecution, error) {
|
func getWorkerBackendExecution(auth string, executionId string) (*shuffle.WorkflowExecution, error) {
|
||||||
backendUrl := os.Getenv("BASE_URL")
|
backendUrl := os.Getenv("BASE_URL")
|
||||||
if len(backendUrl) == 0 {
|
if len(backendUrl) == 0 {
|
||||||
@@ -2446,9 +2514,9 @@ func getWorkerBackendExecution(auth string, executionId string) (*shuffle.Workfl
|
|||||||
|
|
||||||
streamResultUrl := fmt.Sprintf("%s/api/v1/streams/results", backendUrl)
|
streamResultUrl := fmt.Sprintf("%s/api/v1/streams/results", backendUrl)
|
||||||
topClient := shuffle.GetExternalClient(backendUrl)
|
topClient := shuffle.GetExternalClient(backendUrl)
|
||||||
requestData := shuffle.ActionResult {
|
requestData := shuffle.ActionResult{
|
||||||
Authorization: auth,
|
Authorization: auth,
|
||||||
ExecutionId: executionId,
|
ExecutionId: executionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(requestData)
|
data, err := json.Marshal(requestData)
|
||||||
@@ -2708,7 +2776,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
|||||||
}
|
}
|
||||||
|
|
||||||
if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
|
if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions))
|
log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2726,7 +2794,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
|||||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||||
finished := shuffle.ValidateFinished(ctx, -1, *workflowExecution)
|
finished := shuffle.ValidateFinished(ctx, -1, *workflowExecution)
|
||||||
if !finished {
|
if !finished {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[DEBUG][%s] Handling next node since it's not finished!", workflowExecution.ExecutionId)
|
log.Printf("[DEBUG][%s] Handling next node since it's not finished!", workflowExecution.ExecutionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3188,8 +3256,6 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
log.Printf("[DEBUG] Deploying service for %s to swarm on port %d", name, deployport)
|
log.Printf("[DEBUG] Deploying service for %s to swarm on port %d", name, deployport)
|
||||||
//containerName := fmt.Sprintf("shuffle-worker-%s", parsedUuid)
|
//containerName := fmt.Sprintf("shuffle-worker-%s", parsedUuid)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the image exists or not - just in case
|
// Check if the image exists or not - just in case
|
||||||
_, _, err := dockercli.ImageInspectWithRaw(context.Background(), image)
|
_, _, err := dockercli.ImageInspectWithRaw(context.Background(), image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3202,8 +3268,8 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := dockercli.ImagePull(
|
_, err := dockercli.ImagePull(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
image,
|
image,
|
||||||
dockerimage.PullOptions{},
|
dockerimage.PullOptions{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3212,7 +3278,6 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if len(baseimagename) == 0 || baseimagename == "/" {
|
if len(baseimagename) == 0 || baseimagename == "/" {
|
||||||
baseimagename = "frikky/shuffle"
|
baseimagename = "frikky/shuffle"
|
||||||
//var baseimagename = "frikky/shuffle"
|
//var baseimagename = "frikky/shuffle"
|
||||||
@@ -3245,7 +3310,7 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
|
|
||||||
// Max scale as well
|
// Max scale as well
|
||||||
nodeCount := uint64(1)
|
nodeCount := uint64(1)
|
||||||
if inputReplicas > 0 && inputReplicas < 100 {
|
if inputReplicas > 0 && inputReplicas < 100 {
|
||||||
if replicas != uint64(inputReplicas) {
|
if replicas != uint64(inputReplicas) {
|
||||||
log.Printf("[DEBUG] Overwriting replicas to %d/node as inputReplicas is set to %d", inputReplicas, inputReplicas)
|
log.Printf("[DEBUG] Overwriting replicas to %d/node as inputReplicas is set to %d", inputReplicas, inputReplicas)
|
||||||
}
|
}
|
||||||
@@ -3265,7 +3330,6 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
nodeCount = 1
|
nodeCount = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
replicatedJobs := uint64(replicas * nodeCount)
|
replicatedJobs := uint64(replicas * nodeCount)
|
||||||
log.Printf("[DEBUG] Deploying app with name %s with image %s", name, image)
|
log.Printf("[DEBUG] Deploying app with name %s with image %s", name, image)
|
||||||
|
|
||||||
@@ -3395,12 +3459,12 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Retry deploying the service (once)
|
// Retry deploying the service (once)
|
||||||
if !retry {
|
if !retry {
|
||||||
return deploySwarmService(dockercli, name, image, deployport, -1, true)
|
return deploySwarmService(dockercli, name, image, deployport, -1, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For port mapping.
|
// For port mapping.
|
||||||
if strings.Contains(fmt.Sprintf("%s", err), "InvalidArgument") && strings.Contains(fmt.Sprintf("%s", err), "is already in use") {
|
if strings.Contains(fmt.Sprintf("%s", err), "InvalidArgument") && strings.Contains(fmt.Sprintf("%s", err), "is already in use") {
|
||||||
//log.Printf("\n\n[WARNING] Port %d is already allocated. Trying to deploy on next port.\n\n", deployport)
|
//log.Printf("\n\n[WARNING] Port %d is already allocated. Trying to deploy on next port.\n\n", deployport)
|
||||||
|
|
||||||
@@ -3427,7 +3491,6 @@ func findAppInfo(image, name string, redeploy bool) (int, error) {
|
|||||||
// chance of being successful
|
// chance of being successful
|
||||||
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
|
||||||
|
|
||||||
|
|
||||||
highest := baseport
|
highest := baseport
|
||||||
exposedPort := -1
|
exposedPort := -1
|
||||||
|
|
||||||
@@ -3531,10 +3594,10 @@ func findAppInfo(image, name string, redeploy bool) (int, error) {
|
|||||||
time.Sleep(time.Duration(rand.Intn(4)+8) * time.Second)
|
time.Sleep(time.Duration(rand.Intn(4)+8) * time.Second)
|
||||||
replicas := service.Spec.Mode.Replicated.Replicas
|
replicas := service.Spec.Mode.Replicated.Replicas
|
||||||
err = deploySwarmService(
|
err = deploySwarmService(
|
||||||
dockercli,
|
dockercli,
|
||||||
name,
|
name,
|
||||||
image,
|
image,
|
||||||
exposedPort,
|
exposedPort,
|
||||||
int64(*replicas),
|
int64(*replicas),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -3624,7 +3687,7 @@ func findAppInfoKubernetes(image, name string, env []string) error {
|
|||||||
|
|
||||||
for _, deployment := range deployments.Items {
|
for _, deployment := range deployments.Items {
|
||||||
if deployment.Name == name {
|
if deployment.Name == name {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[DEBUG] Found deployment %s - no need to deploy another", name)
|
log.Printf("[DEBUG] Found deployment %s - no need to deploy another", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3650,7 +3713,6 @@ func initSwarmNetwork() error {
|
|||||||
mtu := 1500
|
mtu := 1500
|
||||||
options["com.docker.network.driver.mtu"] = fmt.Sprintf("%d", mtu)
|
options["com.docker.network.driver.mtu"] = fmt.Sprintf("%d", mtu)
|
||||||
|
|
||||||
|
|
||||||
ingressOptions := network.CreateOptions{
|
ingressOptions := network.CreateOptions{
|
||||||
Driver: "overlay",
|
Driver: "overlay",
|
||||||
Attachable: false,
|
Attachable: false,
|
||||||
@@ -3734,11 +3796,9 @@ func initSwarmNetwork() error {
|
|||||||
log.Printf("[WARNING] Swarm Executions network may already exist: %s", err)
|
log.Printf("[WARNING] Swarm Executions network may already exist: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*** ENDREMOVE ***/
|
/*** ENDREMOVE ***/
|
||||||
|
|
||||||
func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, action *shuffle.Action, workflowExecution *shuffle.WorkflowExecution, image string, attempts int64) error {
|
func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, action *shuffle.Action, workflowExecution *shuffle.WorkflowExecution, image string, attempts int64) error {
|
||||||
@@ -3829,6 +3889,12 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
|
|||||||
|
|
||||||
// Checking as LATE as possible, ensuring we don't rerun what's already ran
|
// Checking as LATE as possible, ensuring we don't rerun what's already ran
|
||||||
// ctx = context.Background()
|
// ctx = context.Background()
|
||||||
|
|
||||||
|
// Sleep between 0 and 250 ms for randomness so no same worker check at same time (same as cloud)
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
randMs := rand.Intn(250)
|
||||||
|
time.Sleep(time.Duration(randMs) * time.Millisecond)
|
||||||
|
|
||||||
newExecId := fmt.Sprintf("%s_%s", workflowExecution.ExecutionId, action.ID)
|
newExecId := fmt.Sprintf("%s_%s", workflowExecution.ExecutionId, action.ID)
|
||||||
_, err = shuffle.GetCache(ctx, newExecId)
|
_, err = shuffle.GetCache(ctx, newExecId)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -3889,18 +3955,18 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try redeployment
|
// Try redeployment
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts < 2 {
|
if attempts < 2 {
|
||||||
// Check the service and fix it.
|
// Check the service and fix it.
|
||||||
if isKubernetes == "true" {
|
if isKubernetes == "true" {
|
||||||
log.Printf("[WARNING] App Redeployment in K8s isn't fully supported yet, but should be done for app %s with image %s.", appName, image)
|
log.Printf("[WARNING] App Redeployment in K8s isn't fully supported yet, but should be done for app %s with image %s.", appName, image)
|
||||||
} else {
|
} else {
|
||||||
_, err = findAppInfo(image, appName, true)
|
_, err = findAppInfo(image, appName, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR][%s] Error re-deploying app %s: %s", workflowExecution.ExecutionId, appName, err)
|
log.Printf("[ERROR][%s] Error re-deploying app %s: %s", workflowExecution.ExecutionId, appName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendAppRequest(ctx, incomingUrl, appName, port, action, workflowExecution, image, attempts)
|
return sendAppRequest(ctx, incomingUrl, appName, port, action, workflowExecution, image, attempts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3926,7 +3992,7 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
|
|||||||
log.Printf("[ERROR] Failed reading app request body body: %s", err)
|
log.Printf("[ERROR] Failed reading app request body body: %s", err)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[DEBUG][%s] NEWRESP (from app): %s", workflowExecution.ExecutionId, string(body))
|
log.Printf("[DEBUG][%s] NEWRESP (from app): %s", workflowExecution.ExecutionId, string(body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4326,7 +4392,7 @@ func checkStandaloneRun() {
|
|||||||
|
|
||||||
// Initial loop etc
|
// Initial loop etc
|
||||||
func main() {
|
func main() {
|
||||||
// Testing swarm auto-replacements. This also tests ports
|
// Testing swarm auto-replacements. This also tests ports
|
||||||
// in rapid succession
|
// in rapid succession
|
||||||
|
|
||||||
checkStandaloneRun()
|
checkStandaloneRun()
|
||||||
@@ -4334,7 +4400,7 @@ func main() {
|
|||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
log.Printf("[INFO] Disabled cleanup due to debug mode (DEBUG=true)")
|
log.Printf("[INFO] Disabled cleanup due to debug mode (DEBUG=true)")
|
||||||
cleanupEnv = "false"
|
cleanupEnv = "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** STARTREMOVE ***/
|
/*** STARTREMOVE ***/
|
||||||
@@ -4784,7 +4850,7 @@ func runWebserver(listener net.Listener) {
|
|||||||
if strings.ToLower(os.Getenv("SHUFFLE_SWARM_CONFIG")) == "run" || strings.ToLower(os.Getenv("SHUFFLE_APP_REPLICAS")) == "" {
|
if strings.ToLower(os.Getenv("SHUFFLE_SWARM_CONFIG")) == "run" || strings.ToLower(os.Getenv("SHUFFLE_APP_REPLICAS")) == "" {
|
||||||
// go AutoScaleApps(ctx, dockercli, maxExecutionsPerMinute)
|
// go AutoScaleApps(ctx, dockercli, maxExecutionsPerMinute)
|
||||||
}
|
}
|
||||||
if strings.ToLower(os.Getenv("SHUFFLE_DEBUG_MEMORY")) == "true" {
|
if strings.ToLower(os.Getenv("SHUFFLE_DEBUG_MEMORY")) == "true" || strings.ToLower(os.Getenv("DEBUG_MEMORY")) == "true" {
|
||||||
r.HandleFunc("/debug/pprof/", pprof.Index)
|
r.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
r.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
|
r.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
|
||||||
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||||
@@ -4818,6 +4884,12 @@ func runWebserver(listener net.Listener) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0x0elliot:
|
||||||
|
// IF we had to rewrite this, we will focus on ONLY auto scale for apps.
|
||||||
|
// i recommend we target executions/minute (?) as a metric.
|
||||||
|
// edge-case: subflows are helped with when worker replicas are higher.
|
||||||
|
// i kind of never want to scale down. at least, not now.
|
||||||
|
// also, algorithm is very broken. executions/worker
|
||||||
func AutoScaleApps(ctx context.Context, client *dockerclient.Client, maxExecutionsPerMinute int) {
|
func AutoScaleApps(ctx context.Context, client *dockerclient.Client, maxExecutionsPerMinute int) {
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user